vector-inspector 0.3.7__py3-none-any.whl → 0.3.8__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/provider_factory.py +97 -0
- vector_inspector/ui/components/splash_window.py +14 -2
- vector_inspector/ui/controllers/__init__.py +1 -0
- vector_inspector/ui/controllers/connection_controller.py +177 -0
- vector_inspector/ui/main_window.py +85 -332
- vector_inspector/ui/main_window_shell.py +106 -0
- vector_inspector/ui/services/__init__.py +1 -0
- vector_inspector/ui/services/dialog_service.py +113 -0
- vector_inspector/ui/tabs.py +64 -0
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.8.dist-info}/METADATA +27 -9
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.8.dist-info}/RECORD +13 -6
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.8.dist-info}/WHEEL +0 -0
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.8.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Factory for creating vector database connections from provider configs."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
5
|
+
from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
|
|
6
|
+
from vector_inspector.core.connections.qdrant_connection import QdrantConnection
|
|
7
|
+
from vector_inspector.core.connections.pinecone_connection import PineconeConnection
|
|
8
|
+
from vector_inspector.core.connections.pgvector_connection import PgVectorConnection
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProviderFactory:
|
|
12
|
+
"""Factory for creating database connections from configuration."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def create(
|
|
16
|
+
provider: str, config: Dict[str, Any], credentials: Dict[str, Any] = None
|
|
17
|
+
) -> VectorDBConnection:
|
|
18
|
+
"""Create a connection object for the specified provider.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
provider: Provider type (chromadb, qdrant, pinecone, pgvector)
|
|
22
|
+
config: Provider-specific configuration
|
|
23
|
+
credentials: Optional credentials (API keys, passwords, etc.)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
VectorDBConnection instance
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If provider is unsupported or configuration is invalid
|
|
30
|
+
"""
|
|
31
|
+
credentials = credentials or {}
|
|
32
|
+
|
|
33
|
+
if provider == "chromadb":
|
|
34
|
+
return ProviderFactory._create_chroma(config, credentials)
|
|
35
|
+
elif provider == "qdrant":
|
|
36
|
+
return ProviderFactory._create_qdrant(config, credentials)
|
|
37
|
+
elif provider == "pinecone":
|
|
38
|
+
return ProviderFactory._create_pinecone(config, credentials)
|
|
39
|
+
elif provider == "pgvector":
|
|
40
|
+
return ProviderFactory._create_pgvector(config, credentials)
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _create_chroma(config: Dict[str, Any], credentials: Dict[str, Any]) -> ChromaDBConnection:
|
|
46
|
+
"""Create a ChromaDB connection."""
|
|
47
|
+
conn_type = config.get("type")
|
|
48
|
+
|
|
49
|
+
if conn_type == "persistent":
|
|
50
|
+
return ChromaDBConnection(path=config.get("path"))
|
|
51
|
+
elif conn_type == "http":
|
|
52
|
+
return ChromaDBConnection(host=config.get("host"), port=config.get("port"))
|
|
53
|
+
else: # ephemeral
|
|
54
|
+
return ChromaDBConnection()
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _create_qdrant(config: Dict[str, Any], credentials: Dict[str, Any]) -> QdrantConnection:
|
|
58
|
+
"""Create a Qdrant connection."""
|
|
59
|
+
conn_type = config.get("type")
|
|
60
|
+
api_key = credentials.get("api_key")
|
|
61
|
+
|
|
62
|
+
if conn_type == "persistent":
|
|
63
|
+
return QdrantConnection(path=config.get("path"))
|
|
64
|
+
elif conn_type == "http":
|
|
65
|
+
return QdrantConnection(
|
|
66
|
+
host=config.get("host"), port=config.get("port"), api_key=api_key
|
|
67
|
+
)
|
|
68
|
+
else: # ephemeral
|
|
69
|
+
return QdrantConnection()
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _create_pinecone(config: Dict[str, Any], credentials: Dict[str, Any]) -> PineconeConnection:
|
|
73
|
+
"""Create a Pinecone connection."""
|
|
74
|
+
api_key = credentials.get("api_key")
|
|
75
|
+
if not api_key:
|
|
76
|
+
raise ValueError("Pinecone requires an API key")
|
|
77
|
+
|
|
78
|
+
return PineconeConnection(api_key=api_key)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _create_pgvector(config: Dict[str, Any], credentials: Dict[str, Any]) -> PgVectorConnection:
|
|
82
|
+
"""Create a PgVector/Postgres connection."""
|
|
83
|
+
conn_type = config.get("type")
|
|
84
|
+
|
|
85
|
+
if conn_type == "http":
|
|
86
|
+
host = config.get("host", "localhost")
|
|
87
|
+
port = int(config.get("port", 5432))
|
|
88
|
+
database = config.get("database")
|
|
89
|
+
user = config.get("user")
|
|
90
|
+
# Prefer password from credentials
|
|
91
|
+
password = credentials.get("password")
|
|
92
|
+
|
|
93
|
+
return PgVectorConnection(
|
|
94
|
+
host=host, port=port, database=database, user=user, password=password
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
raise ValueError("Unsupported connection type for PgVector profile")
|
|
@@ -34,10 +34,22 @@ class SplashWindow(QDialog):
|
|
|
34
34
|
layout.addWidget(github)
|
|
35
35
|
|
|
36
36
|
# About info (reuse About dialog text)
|
|
37
|
-
from vector_inspector.
|
|
37
|
+
from vector_inspector.utils.version import get_app_version
|
|
38
38
|
|
|
39
39
|
about = QTextBrowser()
|
|
40
|
-
|
|
40
|
+
version = get_app_version()
|
|
41
|
+
version_html = (
|
|
42
|
+
f"<h2>Vector Inspector {version}</h2>" if version else "<h2>Vector Inspector</h2>"
|
|
43
|
+
)
|
|
44
|
+
about_text = (
|
|
45
|
+
version_html + "<p>A comprehensive desktop application for visualizing, "
|
|
46
|
+
"querying, and managing multiple vector databases simultaneously.</p>"
|
|
47
|
+
'<p><a href="https://github.com/anthonypdawson/vector-inspector" style="color:#2980b9;">GitHub Project Page</a></p>'
|
|
48
|
+
"<hr />"
|
|
49
|
+
"<p>Built with PySide6</p>"
|
|
50
|
+
"<p><b>New:</b> Pinecone support!</p>"
|
|
51
|
+
)
|
|
52
|
+
about.setHtml(about_text)
|
|
41
53
|
about.setOpenExternalLinks(True)
|
|
42
54
|
about.setMaximumHeight(160)
|
|
43
55
|
layout.addWidget(about)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""UI controllers for managing application logic."""
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Controller for managing connection lifecycle and threading."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
from PySide6.QtCore import QObject, Signal, QThread
|
|
5
|
+
from PySide6.QtWidgets import QMessageBox, QWidget
|
|
6
|
+
|
|
7
|
+
from vector_inspector.core.connection_manager import ConnectionManager, ConnectionState
|
|
8
|
+
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
9
|
+
from vector_inspector.core.provider_factory import ProviderFactory
|
|
10
|
+
from vector_inspector.services.profile_service import ProfileService
|
|
11
|
+
from vector_inspector.ui.components.loading_dialog import LoadingDialog
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConnectionThread(QThread):
|
|
15
|
+
"""Background thread for connecting to database."""
|
|
16
|
+
|
|
17
|
+
finished = Signal(bool, list, str) # success, collections, error_message
|
|
18
|
+
|
|
19
|
+
def __init__(self, connection: VectorDBConnection):
|
|
20
|
+
super().__init__()
|
|
21
|
+
self.connection = connection
|
|
22
|
+
|
|
23
|
+
def run(self):
|
|
24
|
+
"""Connect to database and get collections."""
|
|
25
|
+
try:
|
|
26
|
+
success = self.connection.connect()
|
|
27
|
+
if success:
|
|
28
|
+
collections = self.connection.list_collections()
|
|
29
|
+
self.finished.emit(True, collections, "")
|
|
30
|
+
else:
|
|
31
|
+
self.finished.emit(False, [], "Connection failed")
|
|
32
|
+
except Exception as e:
|
|
33
|
+
self.finished.emit(False, [], str(e))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConnectionController(QObject):
|
|
37
|
+
"""Controller for managing connection operations and lifecycle.
|
|
38
|
+
|
|
39
|
+
This handles:
|
|
40
|
+
- Creating connections from profiles
|
|
41
|
+
- Starting connection threads
|
|
42
|
+
- Handling connection results
|
|
43
|
+
- Managing loading dialogs
|
|
44
|
+
- Emitting signals for UI updates
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
connection_completed = Signal(
|
|
48
|
+
str, bool, list, str
|
|
49
|
+
) # connection_id, success, collections, error
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
connection_manager: ConnectionManager,
|
|
54
|
+
profile_service: ProfileService,
|
|
55
|
+
parent: Optional[QWidget] = None,
|
|
56
|
+
):
|
|
57
|
+
super().__init__(parent)
|
|
58
|
+
self.connection_manager = connection_manager
|
|
59
|
+
self.profile_service = profile_service
|
|
60
|
+
self.parent_widget = parent
|
|
61
|
+
|
|
62
|
+
# State
|
|
63
|
+
self._connection_threads: Dict[str, ConnectionThread] = {}
|
|
64
|
+
self.loading_dialog = LoadingDialog("Loading...", parent)
|
|
65
|
+
|
|
66
|
+
def connect_to_profile(self, profile_id: str) -> bool:
|
|
67
|
+
"""Connect to a profile.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
profile_id: ID of the profile to connect to
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if connection initiated successfully, False otherwise
|
|
74
|
+
"""
|
|
75
|
+
profile_data = self.profile_service.get_profile_with_credentials(profile_id)
|
|
76
|
+
if not profile_data:
|
|
77
|
+
QMessageBox.warning(self.parent_widget, "Error", "Profile not found.")
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
# Check connection limit
|
|
81
|
+
if self.connection_manager.get_connection_count() >= ConnectionManager.MAX_CONNECTIONS:
|
|
82
|
+
QMessageBox.warning(
|
|
83
|
+
self.parent_widget,
|
|
84
|
+
"Connection Limit",
|
|
85
|
+
f"Maximum number of connections ({ConnectionManager.MAX_CONNECTIONS}) reached. "
|
|
86
|
+
"Please close a connection first.",
|
|
87
|
+
)
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
# Create connection
|
|
91
|
+
provider = profile_data["provider"]
|
|
92
|
+
config = profile_data["config"]
|
|
93
|
+
credentials = profile_data.get("credentials", {})
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Create connection object using factory
|
|
97
|
+
connection = ProviderFactory.create(provider, config, credentials)
|
|
98
|
+
|
|
99
|
+
# Register with connection manager, using profile_id as connection_id for persistence
|
|
100
|
+
connection_id = self.connection_manager.create_connection(
|
|
101
|
+
name=profile_data["name"],
|
|
102
|
+
provider=provider,
|
|
103
|
+
connection=connection,
|
|
104
|
+
config=config,
|
|
105
|
+
connection_id=profile_data["id"],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Update state to connecting
|
|
109
|
+
self.connection_manager.update_connection_state(
|
|
110
|
+
connection_id, ConnectionState.CONNECTING
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Connect in background thread
|
|
114
|
+
thread = ConnectionThread(connection)
|
|
115
|
+
thread.finished.connect(
|
|
116
|
+
lambda success, collections, error: self._on_connection_finished(
|
|
117
|
+
connection_id, success, collections, error
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
self._connection_threads[connection_id] = thread
|
|
121
|
+
thread.start()
|
|
122
|
+
|
|
123
|
+
# Show loading dialog
|
|
124
|
+
self.loading_dialog.show_loading(f"Connecting to {profile_data['name']}...")
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
QMessageBox.critical(
|
|
129
|
+
self.parent_widget, "Connection Error", f"Failed to create connection: {e}"
|
|
130
|
+
)
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
def _on_connection_finished(
|
|
134
|
+
self, connection_id: str, success: bool, collections: list, error: str
|
|
135
|
+
):
|
|
136
|
+
"""Handle connection thread completion."""
|
|
137
|
+
self.loading_dialog.hide_loading()
|
|
138
|
+
|
|
139
|
+
# Clean up thread
|
|
140
|
+
thread = self._connection_threads.pop(connection_id, None)
|
|
141
|
+
if thread:
|
|
142
|
+
thread.wait() # Wait for thread to fully finish
|
|
143
|
+
thread.deleteLater()
|
|
144
|
+
|
|
145
|
+
if success:
|
|
146
|
+
# Update state to connected
|
|
147
|
+
self.connection_manager.update_connection_state(
|
|
148
|
+
connection_id, ConnectionState.CONNECTED
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Mark connection as opened first (will show in UI)
|
|
152
|
+
self.connection_manager.mark_connection_opened(connection_id)
|
|
153
|
+
|
|
154
|
+
# Then update collections (UI item now exists to receive them)
|
|
155
|
+
self.connection_manager.update_collections(connection_id, collections)
|
|
156
|
+
else:
|
|
157
|
+
# Update state to error
|
|
158
|
+
self.connection_manager.update_connection_state(
|
|
159
|
+
connection_id, ConnectionState.ERROR, error
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
QMessageBox.warning(
|
|
163
|
+
self.parent_widget, "Connection Failed", f"Failed to connect: {error}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Remove the failed connection
|
|
167
|
+
self.connection_manager.close_connection(connection_id)
|
|
168
|
+
|
|
169
|
+
# Emit signal for UI updates
|
|
170
|
+
self.connection_completed.emit(connection_id, success, collections, error)
|
|
171
|
+
|
|
172
|
+
def cleanup(self):
|
|
173
|
+
"""Clean up connection threads on shutdown."""
|
|
174
|
+
for thread in list(self._connection_threads.values()):
|
|
175
|
+
if thread.isRunning():
|
|
176
|
+
thread.quit()
|
|
177
|
+
thread.wait(1000) # Wait up to 1 second
|
|
@@ -1,63 +1,29 @@
|
|
|
1
1
|
"""Updated main window with multi-database support."""
|
|
2
2
|
|
|
3
3
|
from PySide6.QtWidgets import (
|
|
4
|
-
QMainWindow,
|
|
5
|
-
QWidget,
|
|
6
|
-
QVBoxLayout,
|
|
7
|
-
QHBoxLayout,
|
|
8
|
-
QSplitter,
|
|
9
|
-
QTabWidget,
|
|
10
|
-
QStatusBar,
|
|
11
|
-
QToolBar,
|
|
12
4
|
QMessageBox,
|
|
13
|
-
QInputDialog,
|
|
14
5
|
QLabel,
|
|
15
|
-
QDockWidget,
|
|
16
6
|
QApplication,
|
|
17
7
|
QDialog,
|
|
8
|
+
QToolBar,
|
|
9
|
+
QStatusBar,
|
|
18
10
|
)
|
|
19
|
-
from PySide6.QtCore import Qt,
|
|
11
|
+
from PySide6.QtCore import Qt, QTimer
|
|
20
12
|
from PySide6.QtGui import QAction
|
|
21
13
|
|
|
22
|
-
from vector_inspector.core.connection_manager import ConnectionManager
|
|
14
|
+
from vector_inspector.core.connection_manager import ConnectionManager
|
|
23
15
|
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
24
|
-
from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
|
|
25
|
-
from vector_inspector.core.connections.qdrant_connection import QdrantConnection
|
|
26
|
-
from vector_inspector.core.connections.pinecone_connection import PineconeConnection
|
|
27
|
-
from vector_inspector.core.connections.pgvector_connection import PgVectorConnection
|
|
28
16
|
from vector_inspector.services.profile_service import ProfileService
|
|
29
17
|
from vector_inspector.services.settings_service import SettingsService
|
|
18
|
+
from vector_inspector.ui.main_window_shell import InspectorShell
|
|
30
19
|
from vector_inspector.ui.components.connection_manager_panel import ConnectionManagerPanel
|
|
31
20
|
from vector_inspector.ui.components.profile_manager_panel import ProfileManagerPanel
|
|
32
|
-
from vector_inspector.ui.
|
|
33
|
-
from vector_inspector.ui.
|
|
34
|
-
from vector_inspector.ui.
|
|
35
|
-
from vector_inspector.ui.components.loading_dialog import LoadingDialog
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class ConnectionThread(QThread):
|
|
39
|
-
"""Background thread for connecting to database."""
|
|
40
|
-
|
|
41
|
-
finished = Signal(bool, list, str) # success, collections, error_message
|
|
42
|
-
|
|
43
|
-
def __init__(self, connection):
|
|
44
|
-
super().__init__()
|
|
45
|
-
self.connection = connection
|
|
46
|
-
|
|
47
|
-
def run(self):
|
|
48
|
-
"""Connect to database and get collections."""
|
|
49
|
-
try:
|
|
50
|
-
success = self.connection.connect()
|
|
51
|
-
if success:
|
|
52
|
-
collections = self.connection.list_collections()
|
|
53
|
-
self.finished.emit(True, collections, "")
|
|
54
|
-
else:
|
|
55
|
-
self.finished.emit(False, [], "Connection failed")
|
|
56
|
-
except Exception as e:
|
|
57
|
-
self.finished.emit(False, [], str(e))
|
|
21
|
+
from vector_inspector.ui.tabs import InspectorTabs
|
|
22
|
+
from vector_inspector.ui.controllers.connection_controller import ConnectionController
|
|
23
|
+
from vector_inspector.ui.services.dialog_service import DialogService
|
|
58
24
|
|
|
59
25
|
|
|
60
|
-
class MainWindow(
|
|
26
|
+
class MainWindow(InspectorShell):
|
|
61
27
|
"""Main application window with multi-database support."""
|
|
62
28
|
|
|
63
29
|
def __init__(self):
|
|
@@ -67,11 +33,21 @@ class MainWindow(QMainWindow):
|
|
|
67
33
|
self.connection_manager = ConnectionManager()
|
|
68
34
|
self.profile_service = ProfileService()
|
|
69
35
|
self.settings_service = SettingsService()
|
|
70
|
-
|
|
36
|
+
|
|
37
|
+
# Controller for connection operations
|
|
38
|
+
self.connection_controller = ConnectionController(
|
|
39
|
+
self.connection_manager, self.profile_service, self
|
|
40
|
+
)
|
|
71
41
|
|
|
72
42
|
# State
|
|
73
43
|
self.visualization_view = None
|
|
74
|
-
|
|
44
|
+
|
|
45
|
+
# View references (will be set in _setup_ui)
|
|
46
|
+
self.info_panel = None
|
|
47
|
+
self.metadata_view = None
|
|
48
|
+
self.search_view = None
|
|
49
|
+
self.connection_panel = None
|
|
50
|
+
self.profile_panel = None
|
|
75
51
|
|
|
76
52
|
self.setWindowTitle("Vector Inspector")
|
|
77
53
|
self.setGeometry(100, 100, 1600, 900)
|
|
@@ -101,62 +77,36 @@ class MainWindow(QMainWindow):
|
|
|
101
77
|
print(f"[SplashWindow] Failed to show splash: {e}")
|
|
102
78
|
|
|
103
79
|
def _setup_ui(self):
|
|
104
|
-
"""Setup the main UI layout."""
|
|
105
|
-
#
|
|
106
|
-
central_widget = QWidget()
|
|
107
|
-
self.setCentralWidget(central_widget)
|
|
108
|
-
|
|
109
|
-
layout = QHBoxLayout(central_widget)
|
|
110
|
-
layout.setContentsMargins(5, 5, 5, 5)
|
|
111
|
-
|
|
112
|
-
# Main splitter (left panel | right tabs)
|
|
113
|
-
main_splitter = QSplitter(Qt.Horizontal)
|
|
114
|
-
|
|
115
|
-
# Left panel - Connections and Profiles
|
|
116
|
-
left_panel = QWidget()
|
|
117
|
-
left_layout = QVBoxLayout(left_panel)
|
|
118
|
-
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
119
|
-
|
|
120
|
-
# Create tab widget for connections and profiles
|
|
121
|
-
self.left_tabs = QTabWidget()
|
|
122
|
-
|
|
123
|
-
# Connection manager panel
|
|
80
|
+
"""Setup the main UI layout using InspectorShell."""
|
|
81
|
+
# Left panels - Connections and Profiles
|
|
124
82
|
self.connection_panel = ConnectionManagerPanel(self.connection_manager)
|
|
125
|
-
self.
|
|
83
|
+
self.add_left_panel(self.connection_panel, "Active")
|
|
126
84
|
|
|
127
|
-
# Profile manager panel
|
|
128
85
|
self.profile_panel = ProfileManagerPanel(self.profile_service)
|
|
129
|
-
self.
|
|
130
|
-
|
|
131
|
-
left_layout.addWidget(self.left_tabs)
|
|
86
|
+
self.add_left_panel(self.profile_panel, "Profiles")
|
|
132
87
|
|
|
133
|
-
#
|
|
134
|
-
|
|
88
|
+
# Main content tabs using TabRegistry
|
|
89
|
+
tab_defs = InspectorTabs.get_standard_tabs()
|
|
135
90
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
self.search_view = SearchView(None) # Will be set later
|
|
91
|
+
for i, tab_def in enumerate(tab_defs):
|
|
92
|
+
widget = InspectorTabs.create_tab_widget(tab_def, connection=None)
|
|
93
|
+
self.add_main_tab(widget, tab_def.title)
|
|
140
94
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
95
|
+
# Store references to views (except placeholder)
|
|
96
|
+
if i == InspectorTabs.INFO_TAB:
|
|
97
|
+
self.info_panel = widget
|
|
98
|
+
elif i == InspectorTabs.DATA_TAB:
|
|
99
|
+
self.metadata_view = widget
|
|
100
|
+
elif i == InspectorTabs.SEARCH_TAB:
|
|
101
|
+
self.search_view = widget
|
|
102
|
+
# Visualization is lazy-loaded, so it's a placeholder for now
|
|
145
103
|
|
|
146
104
|
# Set Info tab as default
|
|
147
|
-
self.
|
|
105
|
+
self.set_main_tab_active(InspectorTabs.INFO_TAB)
|
|
148
106
|
|
|
149
107
|
# Connect to tab change to lazy load visualization
|
|
150
108
|
self.tab_widget.currentChanged.connect(self._on_tab_changed)
|
|
151
109
|
|
|
152
|
-
# Add panels to splitter
|
|
153
|
-
main_splitter.addWidget(left_panel)
|
|
154
|
-
main_splitter.addWidget(self.tab_widget)
|
|
155
|
-
main_splitter.setStretchFactor(0, 1)
|
|
156
|
-
main_splitter.setStretchFactor(1, 4)
|
|
157
|
-
|
|
158
|
-
layout.addWidget(main_splitter)
|
|
159
|
-
|
|
160
110
|
def _setup_menu_bar(self):
|
|
161
111
|
"""Setup application menu bar."""
|
|
162
112
|
menubar = self.menuBar()
|
|
@@ -303,17 +253,7 @@ class MainWindow(QMainWindow):
|
|
|
303
253
|
# Show update details dialog
|
|
304
254
|
if not hasattr(self, "_latest_release"):
|
|
305
255
|
return
|
|
306
|
-
|
|
307
|
-
from vector_inspector.services.update_service import UpdateService
|
|
308
|
-
|
|
309
|
-
latest = self._latest_release
|
|
310
|
-
version = latest.get("tag_name", "?")
|
|
311
|
-
notes = latest.get("body", "")
|
|
312
|
-
instructions = UpdateService.get_update_instructions()
|
|
313
|
-
pip_cmd = instructions["pip"]
|
|
314
|
-
github_url = instructions["github"]
|
|
315
|
-
dlg = UpdateDetailsDialog(version, notes, pip_cmd, github_url, self)
|
|
316
|
-
dlg.exec()
|
|
256
|
+
DialogService.show_update_details(self._latest_release, self)
|
|
317
257
|
|
|
318
258
|
def _connect_signals(self):
|
|
319
259
|
"""Connect signals between components."""
|
|
@@ -327,6 +267,9 @@ class MainWindow(QMainWindow):
|
|
|
327
267
|
self.connection_manager.collections_updated.connect(self._on_collections_updated)
|
|
328
268
|
self.connection_manager.connection_opened.connect(self._on_connection_opened)
|
|
329
269
|
|
|
270
|
+
# Connection controller signals
|
|
271
|
+
self.connection_controller.connection_completed.connect(self._on_connection_completed)
|
|
272
|
+
|
|
330
273
|
# Connection panel signals
|
|
331
274
|
self.connection_panel.collection_selected.connect(self._on_collection_selected_from_panel)
|
|
332
275
|
self.connection_panel.add_connection_btn.clicked.connect(self._new_connection_from_profile)
|
|
@@ -334,9 +277,20 @@ class MainWindow(QMainWindow):
|
|
|
334
277
|
# Profile panel signals
|
|
335
278
|
self.profile_panel.connect_profile.connect(self._connect_to_profile)
|
|
336
279
|
|
|
280
|
+
def _on_connection_completed(
|
|
281
|
+
self, connection_id: str, success: bool, collections: list, error: str
|
|
282
|
+
):
|
|
283
|
+
"""Handle connection completed event from controller."""
|
|
284
|
+
if success:
|
|
285
|
+
# Switch to Active connections tab
|
|
286
|
+
self.set_left_panel_active(0)
|
|
287
|
+
self.statusBar().showMessage(
|
|
288
|
+
f"Connected successfully ({len(collections)} collections)", 5000
|
|
289
|
+
)
|
|
290
|
+
|
|
337
291
|
def _on_tab_changed(self, index: int):
|
|
338
292
|
"""Handle tab change - lazy load visualization tab."""
|
|
339
|
-
if index ==
|
|
293
|
+
if index == InspectorTabs.VISUALIZATION_TAB and self.visualization_view is None:
|
|
340
294
|
# Lazy load visualization view
|
|
341
295
|
from vector_inspector.ui.views.visualization_view import VisualizationView
|
|
342
296
|
|
|
@@ -346,9 +300,11 @@ class MainWindow(QMainWindow):
|
|
|
346
300
|
|
|
347
301
|
self.visualization_view = VisualizationView(conn)
|
|
348
302
|
# Replace placeholder with actual view
|
|
349
|
-
self.
|
|
350
|
-
self.
|
|
351
|
-
|
|
303
|
+
self.remove_main_tab(InspectorTabs.VISUALIZATION_TAB)
|
|
304
|
+
self.add_main_tab(
|
|
305
|
+
self.visualization_view, "Visualization", InspectorTabs.VISUALIZATION_TAB
|
|
306
|
+
)
|
|
307
|
+
self.set_main_tab_active(InspectorTabs.VISUALIZATION_TAB)
|
|
352
308
|
|
|
353
309
|
# Set collection if one is already selected
|
|
354
310
|
if active and active.active_collection:
|
|
@@ -386,20 +342,22 @@ class MainWindow(QMainWindow):
|
|
|
386
342
|
if connection_id == self.connection_manager.get_active_connection_id():
|
|
387
343
|
# Show loading immediately when collection changes
|
|
388
344
|
if collection_name:
|
|
389
|
-
self.loading_dialog.show_loading(
|
|
345
|
+
self.connection_controller.loading_dialog.show_loading(
|
|
346
|
+
f"Loading collection '{collection_name}'..."
|
|
347
|
+
)
|
|
390
348
|
QApplication.processEvents()
|
|
391
349
|
try:
|
|
392
350
|
self._update_views_for_collection(collection_name)
|
|
393
351
|
finally:
|
|
394
|
-
self.loading_dialog.hide_loading()
|
|
352
|
+
self.connection_controller.loading_dialog.hide_loading()
|
|
395
353
|
else:
|
|
396
354
|
# Clear collection from views
|
|
397
|
-
self.loading_dialog.show_loading("Clearing collection...")
|
|
355
|
+
self.connection_controller.loading_dialog.show_loading("Clearing collection...")
|
|
398
356
|
QApplication.processEvents()
|
|
399
357
|
try:
|
|
400
358
|
self._update_views_for_collection(None)
|
|
401
359
|
finally:
|
|
402
|
-
self.loading_dialog.hide_loading()
|
|
360
|
+
self.connection_controller.loading_dialog.hide_loading()
|
|
403
361
|
|
|
404
362
|
def _on_collections_updated(self, connection_id: str, collections: list):
|
|
405
363
|
"""Handle collections list updated."""
|
|
@@ -417,7 +375,9 @@ class MainWindow(QMainWindow):
|
|
|
417
375
|
def _on_collection_selected_from_panel(self, connection_id: str, collection_name: str):
|
|
418
376
|
"""Handle collection selection from connection panel."""
|
|
419
377
|
# Show loading dialog while switching collections
|
|
420
|
-
self.loading_dialog.show_loading(
|
|
378
|
+
self.connection_controller.loading_dialog.show_loading(
|
|
379
|
+
f"Loading collection '{collection_name}'..."
|
|
380
|
+
)
|
|
421
381
|
QApplication.processEvents()
|
|
422
382
|
|
|
423
383
|
try:
|
|
@@ -425,7 +385,7 @@ class MainWindow(QMainWindow):
|
|
|
425
385
|
# Just update the views
|
|
426
386
|
self._update_views_for_collection(collection_name)
|
|
427
387
|
finally:
|
|
428
|
-
self.loading_dialog.hide_loading()
|
|
388
|
+
self.connection_controller.loading_dialog.hide_loading()
|
|
429
389
|
|
|
430
390
|
def _update_views_with_connection(self, connection: VectorDBConnection):
|
|
431
391
|
"""Update all views with a new connection."""
|
|
@@ -464,176 +424,20 @@ class MainWindow(QMainWindow):
|
|
|
464
424
|
|
|
465
425
|
def _new_connection_from_profile(self):
|
|
466
426
|
"""Show dialog to create new connection (switches to Profiles tab)."""
|
|
467
|
-
self.
|
|
468
|
-
|
|
469
|
-
self,
|
|
470
|
-
"Connect to Profile",
|
|
471
|
-
"Select a profile from the list and click 'Connect', or click '+' to create a new profile.",
|
|
472
|
-
)
|
|
427
|
+
self.set_left_panel_active(1) # Switch to Profiles tab
|
|
428
|
+
DialogService.show_profile_editor_prompt(self)
|
|
473
429
|
|
|
474
430
|
def _show_profile_editor(self):
|
|
475
431
|
"""Show profile editor to create new profile."""
|
|
476
|
-
self.
|
|
432
|
+
self.set_left_panel_active(1) # Switch to Profiles tab
|
|
477
433
|
self.profile_panel._create_profile()
|
|
478
434
|
|
|
479
435
|
def _connect_to_profile(self, profile_id: str):
|
|
480
|
-
"""Connect to a profile."""
|
|
481
|
-
|
|
482
|
-
if not profile_data:
|
|
483
|
-
QMessageBox.warning(self, "Error", "Profile not found.")
|
|
484
|
-
return
|
|
485
|
-
|
|
486
|
-
# Check connection limit
|
|
487
|
-
if self.connection_manager.get_connection_count() >= ConnectionManager.MAX_CONNECTIONS:
|
|
488
|
-
QMessageBox.warning(
|
|
489
|
-
self,
|
|
490
|
-
"Connection Limit",
|
|
491
|
-
f"Maximum number of connections ({ConnectionManager.MAX_CONNECTIONS}) reached. "
|
|
492
|
-
"Please close a connection first.",
|
|
493
|
-
)
|
|
494
|
-
return
|
|
495
|
-
|
|
496
|
-
# Create connection
|
|
497
|
-
provider = profile_data["provider"]
|
|
498
|
-
config = profile_data["config"]
|
|
499
|
-
credentials = profile_data.get("credentials", {})
|
|
500
|
-
|
|
501
|
-
try:
|
|
502
|
-
# Create connection object
|
|
503
|
-
if provider == "chromadb":
|
|
504
|
-
connection = self._create_chroma_connection(config, credentials)
|
|
505
|
-
elif provider == "qdrant":
|
|
506
|
-
connection = self._create_qdrant_connection(config, credentials)
|
|
507
|
-
elif provider == "pinecone":
|
|
508
|
-
connection = self._create_pinecone_connection(config, credentials)
|
|
509
|
-
elif provider == "pgvector":
|
|
510
|
-
connection = self._create_pgvector_connection(config, credentials)
|
|
511
|
-
else:
|
|
512
|
-
QMessageBox.warning(self, "Error", f"Unsupported provider: {provider}")
|
|
513
|
-
return
|
|
514
|
-
|
|
515
|
-
# Register with connection manager, using profile_id as connection_id for persistence
|
|
516
|
-
connection_id = self.connection_manager.create_connection(
|
|
517
|
-
name=profile_data["name"],
|
|
518
|
-
provider=provider,
|
|
519
|
-
connection=connection,
|
|
520
|
-
config=config,
|
|
521
|
-
connection_id=profile_data["id"],
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
# Update state to connecting
|
|
525
|
-
self.connection_manager.update_connection_state(
|
|
526
|
-
connection_id, ConnectionState.CONNECTING
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
# Connect in background thread
|
|
530
|
-
thread = ConnectionThread(connection)
|
|
531
|
-
thread.finished.connect(
|
|
532
|
-
lambda success, collections, error: self._on_connection_finished(
|
|
533
|
-
connection_id, success, collections, error
|
|
534
|
-
)
|
|
535
|
-
)
|
|
536
|
-
self._connection_threads[connection_id] = thread
|
|
537
|
-
thread.start()
|
|
538
|
-
|
|
539
|
-
# Show loading dialog
|
|
540
|
-
self.loading_dialog.show_loading(f"Connecting to {profile_data['name']}...")
|
|
541
|
-
|
|
542
|
-
except Exception as e:
|
|
543
|
-
QMessageBox.critical(self, "Connection Error", f"Failed to create connection: {e}")
|
|
544
|
-
|
|
545
|
-
def _create_chroma_connection(self, config: dict, credentials: dict) -> ChromaDBConnection:
|
|
546
|
-
"""Create a ChromaDB connection."""
|
|
547
|
-
conn_type = config.get("type")
|
|
548
|
-
|
|
549
|
-
if conn_type == "persistent":
|
|
550
|
-
return ChromaDBConnection(path=config.get("path"))
|
|
551
|
-
elif conn_type == "http":
|
|
552
|
-
return ChromaDBConnection(host=config.get("host"), port=config.get("port"))
|
|
553
|
-
else: # ephemeral
|
|
554
|
-
return ChromaDBConnection()
|
|
555
|
-
|
|
556
|
-
def _create_qdrant_connection(self, config: dict, credentials: dict) -> QdrantConnection:
|
|
557
|
-
"""Create a Qdrant connection."""
|
|
558
|
-
conn_type = config.get("type")
|
|
559
|
-
api_key = credentials.get("api_key")
|
|
560
|
-
|
|
561
|
-
if conn_type == "persistent":
|
|
562
|
-
return QdrantConnection(path=config.get("path"))
|
|
563
|
-
elif conn_type == "http":
|
|
564
|
-
return QdrantConnection(
|
|
565
|
-
host=config.get("host"), port=config.get("port"), api_key=api_key
|
|
566
|
-
)
|
|
567
|
-
else: # ephemeral
|
|
568
|
-
return QdrantConnection()
|
|
569
|
-
|
|
570
|
-
def _create_pinecone_connection(self, config: dict, credentials: dict) -> PineconeConnection:
|
|
571
|
-
"""Create a Pinecone connection."""
|
|
572
|
-
api_key = credentials.get("api_key")
|
|
573
|
-
if not api_key:
|
|
574
|
-
raise ValueError("Pinecone requires an API key")
|
|
575
|
-
|
|
576
|
-
return PineconeConnection(api_key=api_key)
|
|
577
|
-
|
|
578
|
-
def _create_pgvector_connection(self, config: dict, credentials: dict) -> PgVectorConnection:
|
|
579
|
-
"""Create a PgVector/Postgres connection from profile config/credentials."""
|
|
580
|
-
conn_type = config.get("type")
|
|
581
|
-
|
|
582
|
-
# We expect HTTP-style profile for pgvector (host/port + db creds)
|
|
583
|
-
if conn_type == "http":
|
|
584
|
-
host = config.get("host", "localhost")
|
|
585
|
-
port = int(config.get("port", 5432))
|
|
586
|
-
database = config.get("database")
|
|
587
|
-
user = config.get("user")
|
|
588
|
-
# Prefer password from credentials
|
|
589
|
-
password = credentials.get("password")
|
|
590
|
-
|
|
591
|
-
return PgVectorConnection(
|
|
592
|
-
host=host, port=port, database=database, user=user, password=password
|
|
593
|
-
)
|
|
594
|
-
|
|
595
|
-
raise ValueError("Unsupported connection type for PgVector profile")
|
|
596
|
-
|
|
597
|
-
def _on_connection_finished(
|
|
598
|
-
self, connection_id: str, success: bool, collections: list, error: str
|
|
599
|
-
):
|
|
600
|
-
"""Handle connection thread completion."""
|
|
601
|
-
self.loading_dialog.hide_loading()
|
|
602
|
-
|
|
603
|
-
# Clean up thread
|
|
604
|
-
thread = self._connection_threads.pop(connection_id, None)
|
|
605
|
-
if thread:
|
|
606
|
-
thread.wait() # Wait for thread to fully finish
|
|
607
|
-
thread.deleteLater()
|
|
608
|
-
|
|
436
|
+
"""Connect to a profile using the connection controller."""
|
|
437
|
+
success = self.connection_controller.connect_to_profile(profile_id)
|
|
609
438
|
if success:
|
|
610
|
-
#
|
|
611
|
-
self.
|
|
612
|
-
connection_id, ConnectionState.CONNECTED
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
# Mark connection as opened first (will show in UI)
|
|
616
|
-
self.connection_manager.mark_connection_opened(connection_id)
|
|
617
|
-
|
|
618
|
-
# Then update collections (UI item now exists to receive them)
|
|
619
|
-
self.connection_manager.update_collections(connection_id, collections)
|
|
620
|
-
|
|
621
|
-
# Switch to Active connections tab
|
|
622
|
-
self.left_tabs.setCurrentIndex(0)
|
|
623
|
-
|
|
624
|
-
self.statusBar().showMessage(
|
|
625
|
-
f"Connected successfully ({len(collections)} collections)", 5000
|
|
626
|
-
)
|
|
627
|
-
else:
|
|
628
|
-
# Update state to error
|
|
629
|
-
self.connection_manager.update_connection_state(
|
|
630
|
-
connection_id, ConnectionState.ERROR, error
|
|
631
|
-
)
|
|
632
|
-
|
|
633
|
-
QMessageBox.warning(self, "Connection Failed", f"Failed to connect: {error}")
|
|
634
|
-
|
|
635
|
-
# Remove the failed connection
|
|
636
|
-
self.connection_manager.close_connection(connection_id)
|
|
439
|
+
# Switch to Active connections tab after initiating connection
|
|
440
|
+
self.set_left_panel_active(0)
|
|
637
441
|
|
|
638
442
|
def _refresh_active_connection(self):
|
|
639
443
|
"""Refresh collections for the active connection."""
|
|
@@ -665,13 +469,7 @@ class MainWindow(QMainWindow):
|
|
|
665
469
|
|
|
666
470
|
def _show_about(self):
|
|
667
471
|
"""Show about dialog."""
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
QMessageBox.about(
|
|
671
|
-
self,
|
|
672
|
-
"About Vector Inspector",
|
|
673
|
-
get_about_html(),
|
|
674
|
-
)
|
|
472
|
+
DialogService.show_about(self)
|
|
675
473
|
|
|
676
474
|
def _toggle_cache(self, checked: bool):
|
|
677
475
|
"""Toggle caching on/off."""
|
|
@@ -681,53 +479,25 @@ class MainWindow(QMainWindow):
|
|
|
681
479
|
|
|
682
480
|
def _show_migration_dialog(self):
|
|
683
481
|
"""Show cross-database migration dialog."""
|
|
684
|
-
|
|
685
|
-
QMessageBox.information(
|
|
686
|
-
self,
|
|
687
|
-
"Insufficient Connections",
|
|
688
|
-
"You need at least 2 active connections to migrate data.\n"
|
|
689
|
-
"Please connect to additional databases first.",
|
|
690
|
-
)
|
|
691
|
-
return
|
|
692
|
-
|
|
693
|
-
from vector_inspector.ui.dialogs.cross_db_migration import CrossDatabaseMigrationDialog
|
|
694
|
-
|
|
695
|
-
dialog = CrossDatabaseMigrationDialog(self.connection_manager, self)
|
|
696
|
-
dialog.exec()
|
|
482
|
+
DialogService.show_migration_dialog(self.connection_manager, self)
|
|
697
483
|
|
|
698
484
|
def _show_backup_restore_dialog(self):
|
|
699
485
|
"""Show backup/restore dialog for the active collection."""
|
|
700
|
-
#
|
|
486
|
+
# Get active connection and collection
|
|
701
487
|
connection = self.connection_manager.get_active_connection()
|
|
702
|
-
if not connection:
|
|
703
|
-
QMessageBox.information(self, "No Connection", "Please connect to a database first.")
|
|
704
|
-
return
|
|
705
|
-
|
|
706
|
-
# Get active collection
|
|
707
488
|
collection_name = self.connection_manager.get_active_collection()
|
|
708
|
-
if not collection_name:
|
|
709
|
-
# Allow opening dialog without a collection selected (for restore-only)
|
|
710
|
-
QMessageBox.information(
|
|
711
|
-
self,
|
|
712
|
-
"No Collection Selected",
|
|
713
|
-
"You can restore backups without a collection selected.\n"
|
|
714
|
-
"To create a backup, please select a collection first.",
|
|
715
|
-
)
|
|
716
489
|
|
|
717
|
-
|
|
490
|
+
# Show dialog
|
|
491
|
+
result = DialogService.show_backup_restore_dialog(connection, collection_name or "", self)
|
|
718
492
|
|
|
719
|
-
|
|
720
|
-
if dialog.exec() == QDialog.Accepted:
|
|
493
|
+
if result == QDialog.Accepted:
|
|
721
494
|
# Refresh collections after restore
|
|
722
495
|
self._refresh_active_connection()
|
|
723
496
|
|
|
724
497
|
def closeEvent(self, event):
|
|
725
498
|
"""Handle application close."""
|
|
726
|
-
#
|
|
727
|
-
|
|
728
|
-
if thread.isRunning():
|
|
729
|
-
thread.quit()
|
|
730
|
-
thread.wait(1000) # Wait up to 1 second
|
|
499
|
+
# Clean up connection controller
|
|
500
|
+
self.connection_controller.cleanup()
|
|
731
501
|
|
|
732
502
|
# Clean up temp HTML files from visualization view
|
|
733
503
|
if self.visualization_view is not None:
|
|
@@ -739,20 +509,3 @@ class MainWindow(QMainWindow):
|
|
|
739
509
|
self.connection_manager.close_all_connections()
|
|
740
510
|
|
|
741
511
|
event.accept()
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
def get_about_html():
|
|
745
|
-
from vector_inspector.utils.version import get_app_version
|
|
746
|
-
|
|
747
|
-
version = get_app_version()
|
|
748
|
-
version_html = (
|
|
749
|
-
f"<h2>Vector Inspector {version}</h2>" if version else "<h2>Vector Inspector</h2>"
|
|
750
|
-
)
|
|
751
|
-
return (
|
|
752
|
-
version_html + "<p>A comprehensive desktop application for visualizing, "
|
|
753
|
-
"querying, and managing multiple vector databases simultaneously.</p>"
|
|
754
|
-
'<p><a href="https://github.com/anthonypdawson/vector-inspector" style="color:#2980b9;">GitHub Project Page</a></p>'
|
|
755
|
-
"<hr />"
|
|
756
|
-
"<p>Built with PySide6</p>"
|
|
757
|
-
"<p><b>New:</b> Pinecone support!</p>"
|
|
758
|
-
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Reusable UI shell for Vector Inspector applications."""
|
|
2
|
+
|
|
3
|
+
from PySide6.QtWidgets import (
|
|
4
|
+
QMainWindow,
|
|
5
|
+
QWidget,
|
|
6
|
+
QVBoxLayout,
|
|
7
|
+
QHBoxLayout,
|
|
8
|
+
QSplitter,
|
|
9
|
+
QTabWidget,
|
|
10
|
+
)
|
|
11
|
+
from PySide6.QtCore import Qt
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InspectorShell(QMainWindow):
|
|
15
|
+
"""Base shell for Inspector applications with splitter, tab widget, and left panel.
|
|
16
|
+
|
|
17
|
+
This provides the basic UI structure that can be reused by Vector Inspector
|
|
18
|
+
and Vector Fusion Studio. Subclasses customize behavior and add domain logic.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
# Main UI components that subclasses will interact with
|
|
25
|
+
self.left_tabs = None
|
|
26
|
+
self.tab_widget = None
|
|
27
|
+
self.main_splitter = None
|
|
28
|
+
|
|
29
|
+
self._setup_shell_ui()
|
|
30
|
+
|
|
31
|
+
def _setup_shell_ui(self):
|
|
32
|
+
"""Setup the main UI shell layout."""
|
|
33
|
+
# Central widget with splitter
|
|
34
|
+
central_widget = QWidget()
|
|
35
|
+
self.setCentralWidget(central_widget)
|
|
36
|
+
|
|
37
|
+
layout = QHBoxLayout(central_widget)
|
|
38
|
+
layout.setContentsMargins(5, 5, 5, 5)
|
|
39
|
+
|
|
40
|
+
# Main splitter (left panel | right tabs)
|
|
41
|
+
self.main_splitter = QSplitter(Qt.Horizontal)
|
|
42
|
+
|
|
43
|
+
# Left panel container (will hold tabs)
|
|
44
|
+
left_panel = QWidget()
|
|
45
|
+
left_layout = QVBoxLayout(left_panel)
|
|
46
|
+
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
47
|
+
|
|
48
|
+
# Create tab widget for left panel
|
|
49
|
+
self.left_tabs = QTabWidget()
|
|
50
|
+
left_layout.addWidget(self.left_tabs)
|
|
51
|
+
|
|
52
|
+
# Right panel - main content tabs
|
|
53
|
+
self.tab_widget = QTabWidget()
|
|
54
|
+
|
|
55
|
+
# Add panels to splitter
|
|
56
|
+
self.main_splitter.addWidget(left_panel)
|
|
57
|
+
self.main_splitter.addWidget(self.tab_widget)
|
|
58
|
+
self.main_splitter.setStretchFactor(0, 1)
|
|
59
|
+
self.main_splitter.setStretchFactor(1, 4)
|
|
60
|
+
|
|
61
|
+
layout.addWidget(self.main_splitter)
|
|
62
|
+
|
|
63
|
+
def add_left_panel(self, widget: QWidget, title: str, index: int = -1):
|
|
64
|
+
"""Add a panel to the left tab widget.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
widget: The panel widget to add
|
|
68
|
+
title: Display title for the tab
|
|
69
|
+
index: Optional position (default appends to end)
|
|
70
|
+
"""
|
|
71
|
+
if index < 0:
|
|
72
|
+
self.left_tabs.addTab(widget, title)
|
|
73
|
+
else:
|
|
74
|
+
self.left_tabs.insertTab(index, widget, title)
|
|
75
|
+
|
|
76
|
+
def add_main_tab(self, widget: QWidget, title: str, index: int = -1):
|
|
77
|
+
"""Add a tab to the main content area.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
widget: The tab widget to add
|
|
81
|
+
title: Display title for the tab
|
|
82
|
+
index: Optional position (default appends to end)
|
|
83
|
+
"""
|
|
84
|
+
if index < 0:
|
|
85
|
+
self.tab_widget.addTab(widget, title)
|
|
86
|
+
else:
|
|
87
|
+
self.tab_widget.insertTab(index, widget, title)
|
|
88
|
+
|
|
89
|
+
def set_left_panel_active(self, index: int):
|
|
90
|
+
"""Switch to a specific left panel tab."""
|
|
91
|
+
if 0 <= index < self.left_tabs.count():
|
|
92
|
+
self.left_tabs.setCurrentIndex(index)
|
|
93
|
+
|
|
94
|
+
def set_main_tab_active(self, index: int):
|
|
95
|
+
"""Switch to a specific main content tab."""
|
|
96
|
+
if 0 <= index < self.tab_widget.count():
|
|
97
|
+
self.tab_widget.setCurrentIndex(index)
|
|
98
|
+
|
|
99
|
+
def get_main_tab_count(self) -> int:
|
|
100
|
+
"""Get the number of main content tabs."""
|
|
101
|
+
return self.tab_widget.count()
|
|
102
|
+
|
|
103
|
+
def remove_main_tab(self, index: int):
|
|
104
|
+
"""Remove a main content tab."""
|
|
105
|
+
if 0 <= index < self.tab_widget.count():
|
|
106
|
+
self.tab_widget.removeTab(index)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""UI services for dialog management and utilities."""
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Service for managing application dialogs."""
|
|
2
|
+
|
|
3
|
+
from PySide6.QtWidgets import QMessageBox, QDialog, QWidget
|
|
4
|
+
from vector_inspector.core.connection_manager import ConnectionManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DialogService:
|
|
8
|
+
"""Service for launching application dialogs."""
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def show_about(parent: QWidget = None):
|
|
12
|
+
"""Show about dialog."""
|
|
13
|
+
from vector_inspector.utils.version import get_app_version
|
|
14
|
+
|
|
15
|
+
version = get_app_version()
|
|
16
|
+
version_html = (
|
|
17
|
+
f"<h2>Vector Inspector {version}</h2>" if version else "<h2>Vector Inspector</h2>"
|
|
18
|
+
)
|
|
19
|
+
about_text = (
|
|
20
|
+
version_html + "<p>A comprehensive desktop application for visualizing, "
|
|
21
|
+
"querying, and managing multiple vector databases simultaneously.</p>"
|
|
22
|
+
'<p><a href="https://github.com/anthonypdawson/vector-inspector" style="color:#2980b9;">GitHub Project Page</a></p>'
|
|
23
|
+
"<hr />"
|
|
24
|
+
"<p>Built with PySide6</p>"
|
|
25
|
+
"<p><b>New:</b> Pinecone support!</p>"
|
|
26
|
+
)
|
|
27
|
+
QMessageBox.about(parent, "About Vector Inspector", about_text)
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def show_backup_restore_dialog(
|
|
31
|
+
connection, collection_name: str = "", parent: QWidget = None
|
|
32
|
+
) -> int:
|
|
33
|
+
"""Show backup/restore dialog.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
connection: Active connection instance
|
|
37
|
+
collection_name: Optional collection name
|
|
38
|
+
parent: Parent widget
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
QDialog.Accepted or QDialog.Rejected
|
|
42
|
+
"""
|
|
43
|
+
if not connection:
|
|
44
|
+
QMessageBox.information(parent, "No Connection", "Please connect to a database first.")
|
|
45
|
+
return QDialog.Rejected
|
|
46
|
+
|
|
47
|
+
# Show info if no collection selected
|
|
48
|
+
if not collection_name:
|
|
49
|
+
QMessageBox.information(
|
|
50
|
+
parent,
|
|
51
|
+
"No Collection Selected",
|
|
52
|
+
"You can restore backups without a collection selected.\n"
|
|
53
|
+
"To create a backup, please select a collection first.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
from vector_inspector.ui.components.backup_restore_dialog import BackupRestoreDialog
|
|
57
|
+
|
|
58
|
+
dialog = BackupRestoreDialog(connection, collection_name or "", parent)
|
|
59
|
+
return dialog.exec()
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def show_migration_dialog(connection_manager: ConnectionManager, parent: QWidget = None) -> int:
|
|
63
|
+
"""Show cross-database migration dialog.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
connection_manager: Connection manager instance
|
|
67
|
+
parent: Parent widget
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
QDialog.Accepted or QDialog.Rejected
|
|
71
|
+
"""
|
|
72
|
+
if connection_manager.get_connection_count() < 2:
|
|
73
|
+
QMessageBox.information(
|
|
74
|
+
parent,
|
|
75
|
+
"Insufficient Connections",
|
|
76
|
+
"You need at least 2 active connections to migrate data.\n"
|
|
77
|
+
"Please connect to additional databases first.",
|
|
78
|
+
)
|
|
79
|
+
return QDialog.Rejected
|
|
80
|
+
|
|
81
|
+
from vector_inspector.ui.dialogs.cross_db_migration import CrossDatabaseMigrationDialog
|
|
82
|
+
|
|
83
|
+
dialog = CrossDatabaseMigrationDialog(connection_manager, parent)
|
|
84
|
+
return dialog.exec()
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def show_profile_editor_prompt(parent: QWidget = None):
|
|
88
|
+
"""Show message prompting user to create a new profile."""
|
|
89
|
+
QMessageBox.information(
|
|
90
|
+
parent,
|
|
91
|
+
"Connect to Profile",
|
|
92
|
+
"Select a profile from the list and click 'Connect', or click '+' to create a new profile.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def show_update_details(latest_release: dict, parent: QWidget = None):
|
|
97
|
+
"""Show update details dialog.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
latest_release: Latest release info from GitHub API
|
|
101
|
+
parent: Parent widget
|
|
102
|
+
"""
|
|
103
|
+
from vector_inspector.ui.components.update_details_dialog import UpdateDetailsDialog
|
|
104
|
+
from vector_inspector.services.update_service import UpdateService
|
|
105
|
+
|
|
106
|
+
version = latest_release.get("tag_name", "?")
|
|
107
|
+
notes = latest_release.get("body", "")
|
|
108
|
+
instructions = UpdateService.get_update_instructions()
|
|
109
|
+
pip_cmd = instructions["pip"]
|
|
110
|
+
github_url = instructions["github"]
|
|
111
|
+
|
|
112
|
+
dialog = UpdateDetailsDialog(version, notes, pip_cmd, github_url, parent)
|
|
113
|
+
dialog.exec()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Tab registry for Inspector applications."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Tuple, Type
|
|
4
|
+
from PySide6.QtWidgets import QWidget
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TabDefinition:
|
|
8
|
+
"""Definition for a tab in the main content area."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, title: str, widget_class: Type[QWidget], lazy_load: bool = False):
|
|
11
|
+
self.title = title
|
|
12
|
+
self.widget_class = widget_class
|
|
13
|
+
self.lazy_load = lazy_load
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InspectorTabs:
|
|
17
|
+
"""Registry of standard Inspector tabs.
|
|
18
|
+
|
|
19
|
+
This allows both Vector Inspector and Vector Fusion Studio to use
|
|
20
|
+
the same tab definitions and add their own custom tabs.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Tab indices (for programmatic access)
|
|
24
|
+
INFO_TAB = 0
|
|
25
|
+
DATA_TAB = 1
|
|
26
|
+
SEARCH_TAB = 2
|
|
27
|
+
VISUALIZATION_TAB = 3
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def get_standard_tabs() -> List[TabDefinition]:
|
|
31
|
+
"""Get list of standard Inspector tabs.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of TabDefinition objects
|
|
35
|
+
"""
|
|
36
|
+
from vector_inspector.ui.views.info_panel import InfoPanel
|
|
37
|
+
from vector_inspector.ui.views.metadata_view import MetadataView
|
|
38
|
+
from vector_inspector.ui.views.search_view import SearchView
|
|
39
|
+
from vector_inspector.ui.views.visualization_view import VisualizationView
|
|
40
|
+
|
|
41
|
+
return [
|
|
42
|
+
TabDefinition("Info", InfoPanel, lazy_load=False),
|
|
43
|
+
TabDefinition("Data Browser", MetadataView, lazy_load=False),
|
|
44
|
+
TabDefinition("Search", SearchView, lazy_load=False),
|
|
45
|
+
TabDefinition("Visualization", VisualizationView, lazy_load=True),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def create_tab_widget(tab_def: TabDefinition, connection=None) -> QWidget:
|
|
50
|
+
"""Create a widget instance from a tab definition.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
tab_def: Tab definition
|
|
54
|
+
connection: Optional connection to pass to widget
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Widget instance
|
|
58
|
+
"""
|
|
59
|
+
if tab_def.lazy_load:
|
|
60
|
+
# Return placeholder for lazy-loaded tabs
|
|
61
|
+
return QWidget()
|
|
62
|
+
else:
|
|
63
|
+
# Create widget with connection
|
|
64
|
+
return tab_def.widget_class(connection)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: vector-inspector
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: A comprehensive desktop application for visualizing, querying, and managing vector database data
|
|
5
5
|
Author-Email: Anthony Dawson <anthonypdawson+github@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -31,13 +31,31 @@ Requires-Dist: pgvector>=0.4.2
|
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
32
|
|
|
33
33
|
# Latest updates
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
|
|
35
|
+
## Vector Inspector 2026.01 Release Notes
|
|
36
|
+
|
|
37
|
+
### Major Refactor and Studio-Ready Architecture
|
|
38
|
+
- Refactored main window into modular components:
|
|
39
|
+
- InspectorShell: reusable UI shell (splitter, tabs, layout)
|
|
40
|
+
- ProviderFactory: centralized connection creation
|
|
41
|
+
- DialogService: dialog management
|
|
42
|
+
- ConnectionController: connection lifecycle and threading
|
|
43
|
+
- InspectorTabs: pluggable tab registry
|
|
44
|
+
- MainWindow now inherits from InspectorShell and is fully reusable as a widget
|
|
45
|
+
- Bootstrap logic is separated from UI logic—Studio can host Inspector as a component
|
|
46
|
+
- Tab system is now pluggable: Studio and Inspector can add, remove, or override tabs via TabDefinition
|
|
47
|
+
- All Inspector UI logic is self-contained; Studio can extend without modifying Inspector code
|
|
48
|
+
|
|
49
|
+
### Data Browser Improvements
|
|
50
|
+
- Added a checkbox: Generate embeddings on edit (default: checked)
|
|
51
|
+
- When unchecked, editing a row skips embedding regeneration
|
|
52
|
+
- Setting is persisted per user
|
|
53
|
+
|
|
54
|
+
### Developer and Architecture Notes
|
|
55
|
+
- All modules pass syntax checks and are ready for Studio integration
|
|
56
|
+
- No breaking changes for existing Inspector users
|
|
57
|
+
- Inspector is now a true UI module, not just an application
|
|
58
|
+
|
|
41
59
|
---
|
|
42
60
|
|
|
43
61
|
# Vector Inspector
|
|
@@ -45,7 +63,7 @@ Description-Content-Type: text/markdown
|
|
|
45
63
|
> **Disclaimer:** This tool is currently under active development and is **not production ready**. Not all features have been thoroughly tested and code is released frequently. Use with caution in critical or production environments.
|
|
46
64
|
|
|
47
65
|
[](https://github.com/anthonypdawson/vector-inspector/actions/workflows/ci-tests.yml)
|
|
48
|
-
[](https://github.com/anthonypdawson/vector-inspector/actions/workflows/publish.yml)
|
|
66
|
+
[](https://github.com/anthonypdawson/vector-inspector/actions/workflows/publish%20copy.yml)
|
|
49
67
|
|
|
50
68
|
[](https://pypi.org/project/vector-inspector/)
|
|
51
69
|
[](https://pepy.tech/projects/vector-inspector)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
vector_inspector-0.3.
|
|
2
|
-
vector_inspector-0.3.
|
|
3
|
-
vector_inspector-0.3.
|
|
1
|
+
vector_inspector-0.3.8.dist-info/METADATA,sha256=efuN0ywBf5QkEwGbRDiOV9YHG9pLNdBWBqnHPDsqUg0,11574
|
|
2
|
+
vector_inspector-0.3.8.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
vector_inspector-0.3.8.dist-info/entry_points.txt,sha256=u96envMI2NFImZUJDFutiiWl7ZoHrrev9joAgtyvTxo,80
|
|
4
4
|
vector_inspector/__init__.py,sha256=Q8XbXn98o0eliQWPePhy-aGUz2KNnVg7bQq-sBPl7zQ,119
|
|
5
5
|
vector_inspector/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
|
|
6
6
|
vector_inspector/config/__init__.py,sha256=vHkVsXSUdInsfzWSOLPZzaaELa3SGenAgfpY5EYbsYA,95
|
|
@@ -26,6 +26,7 @@ vector_inspector/core/embedding_providers/sentence_transformer_provider.py,sha25
|
|
|
26
26
|
vector_inspector/core/embedding_utils.py,sha256=UCnJllDS_YPqbOPVo_kxSCUxM64C5tmcH-fuU9IUifQ,5558
|
|
27
27
|
vector_inspector/core/logging.py,sha256=HQ6_OZgZmaS3OMFOTAqc0oRbZujqo1W0w8OU4viXP1g,845
|
|
28
28
|
vector_inspector/core/model_registry.py,sha256=fdofceD3iyNpECVC7djTEAaDYgHX_7JQ3ROh5A0plpY,6269
|
|
29
|
+
vector_inspector/core/provider_factory.py,sha256=QFDpJTOBJVYAdOLlY0GxSWl87Yj_UT9ZzOm9cjVsGMU,3924
|
|
29
30
|
vector_inspector/main.py,sha256=iZeMPH94q6Ma92hWQLJ7on3rwxytUoS0V8n9MkAuEaY,595
|
|
30
31
|
vector_inspector/services/__init__.py,sha256=QLgH7oybjHuEYDFNiBgmJxvSpgAzHEuBEPXa3SKJb_I,67
|
|
31
32
|
vector_inspector/services/backup_helpers.py,sha256=aX1ONFegERq6dpoNM1eJrbyE1gWCV3SuUHMyPpnxrYM,2005
|
|
@@ -45,13 +46,19 @@ vector_inspector/ui/components/filter_builder.py,sha256=NSR_hp-rzUZVAca6dIJhTxZA
|
|
|
45
46
|
vector_inspector/ui/components/item_dialog.py,sha256=VMwehEjQ6xrdxWygR9J-hHsLfzOVb_E3ePUGYO_c7XA,3951
|
|
46
47
|
vector_inspector/ui/components/loading_dialog.py,sha256=YEKYGU-R-Zz4CjXSArJtkNxgTy4O9hI5Bbt6qlIzD8U,1018
|
|
47
48
|
vector_inspector/ui/components/profile_manager_panel.py,sha256=U-Ea6KC97ltj7bYtG4h9Okb97SbfBAvH1SusbYHTn1o,27930
|
|
48
|
-
vector_inspector/ui/components/splash_window.py,sha256=
|
|
49
|
+
vector_inspector/ui/components/splash_window.py,sha256=AYuRRdLfnuS3ueMSCLfpGG4Q348g_uDIGRyhK4ovzzU,2507
|
|
49
50
|
vector_inspector/ui/components/update_details_dialog.py,sha256=IQl4QZ30tHHOFay68qGOajQ6JuOB85plNoEazzMYS6k,1587
|
|
51
|
+
vector_inspector/ui/controllers/__init__.py,sha256=Wrc4GsLBbIU3FFk9py9URcblNCp-auGAAT1XPi20nWY,53
|
|
52
|
+
vector_inspector/ui/controllers/connection_controller.py,sha256=cx5Vgdn7cleMeJdUewDHd4NXSMmPUOFDwxQQVArfXuE,6494
|
|
50
53
|
vector_inspector/ui/dialogs/__init__.py,sha256=xtT77L91PFfm3zHYRENHkWHJaKPm1htuUzRXAF53P8w,211
|
|
51
54
|
vector_inspector/ui/dialogs/cross_db_migration.py,sha256=BaUyic8l5Ywwql2hQyxVrCXHMjGtqerNAQHDYxcbQ54,15872
|
|
52
55
|
vector_inspector/ui/dialogs/embedding_config_dialog.py,sha256=1K5LBSBXp590BvKwtHx9qgPwGREsn1mJ8cjFGSZHnMA,12926
|
|
53
56
|
vector_inspector/ui/dialogs/provider_type_dialog.py,sha256=W_FAJuvicwBUJJ7PyvKow9lc8_a5pnE3RIAsh-DVndQ,6809
|
|
54
|
-
vector_inspector/ui/main_window.py,sha256=
|
|
57
|
+
vector_inspector/ui/main_window.py,sha256=mZfUm29Ur-dfR4pbHXRSaDsQmIHFY1_SfOahHE_o1DI,21493
|
|
58
|
+
vector_inspector/ui/main_window_shell.py,sha256=0o4KxRc4KXu-mJxni9dv74a5DzP4OIvJoLTX7BLqDoo,3425
|
|
59
|
+
vector_inspector/ui/services/__init__.py,sha256=m2DGkhYlcQQGMtNQsup5eKmhCFhOhXHi-g9Hw0GH1vE,55
|
|
60
|
+
vector_inspector/ui/services/dialog_service.py,sha256=1NHWSMvNadcmoh8tgUMSa8N7g8xYDOTaWMr1G8i9e8A,4261
|
|
61
|
+
vector_inspector/ui/tabs.py,sha256=nniOLax93udxFt1t3s-kx1BpguXBEiDUmC1HA9J-scw,2071
|
|
55
62
|
vector_inspector/ui/views/__init__.py,sha256=FeMtVzSbVFBMjdwLQSQqD0FRW4ieJ4ZKXtTBci2e_bw,30
|
|
56
63
|
vector_inspector/ui/views/collection_browser.py,sha256=oG9_YGPoVuMs-f_zSd4EcITmEU9caxvwuubsFUrNf-c,3991
|
|
57
64
|
vector_inspector/ui/views/connection_view.py,sha256=3oGbClqwpVuUD3AIT8TuM-8heDvwMYw7RowHT3b1b8o,23749
|
|
@@ -62,4 +69,4 @@ vector_inspector/ui/views/visualization_view.py,sha256=wgkSkOM-ShOHDj1GCUtKnqH87
|
|
|
62
69
|
vector_inspector/utils/__init__.py,sha256=jhHBQC8C8bfhNlf6CAt07ejjStp_YAyleaYr2dm0Dk0,38
|
|
63
70
|
vector_inspector/utils/lazy_imports.py,sha256=2XZ3ZnwTvZ5vvrh36nJ_TUjwwkgjoAED6i6P9yctvt0,1211
|
|
64
71
|
vector_inspector/utils/version.py,sha256=2Xk9DEKlDRGEszNNiYnK7ps1i3OH56H2uZhR0_yZORs,382
|
|
65
|
-
vector_inspector-0.3.
|
|
72
|
+
vector_inspector-0.3.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|