Skip to content

Commit b13eef4

Browse files
committed
Add database connection modal to welcome overlay
1 parent d4e9d2a commit b13eef4

1 file changed

Lines changed: 276 additions & 1 deletion

File tree

sweet/ui/widgets.py

Lines changed: 276 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
RadioSet,
2626
Select,
2727
Static,
28+
TabbedContent,
29+
TabPane,
2830
TextArea,
2931
)
3032

@@ -93,6 +95,9 @@ def compose(self) -> ComposeResult:
9395
"Paste from Clipboard", id="welcome-paste-clipboard", classes="welcome-button"
9496
)
9597
with Horizontal(classes="welcome-buttons"):
98+
yield Button(
99+
"Connect to Database", id="welcome-connect-database", classes="welcome-button"
100+
)
96101
yield Button("Exit Sweet", id="welcome-exit", classes="welcome-button")
97102
yield Static("", classes="spacer") # Bottom spacer
98103

@@ -124,6 +129,11 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
124129
elif event.button.id == "welcome-paste-clipboard":
125130
self.log("Calling action_paste_from_clipboard")
126131
data_grid.action_paste_from_clipboard()
132+
elif event.button.id == "welcome-connect-database":
133+
self.log("Opening database connection modal")
134+
self.app.push_screen(
135+
DatabaseConnectionModal(), self._handle_database_connection
136+
)
127137
else:
128138
self.log(f"Data grid not found, parent.parent is: {type(data_grid)}")
129139
except Exception as e:
@@ -223,7 +233,7 @@ def _navigate_buttons_vertical(self, direction: int) -> None:
223233
"welcome-load-sample",
224234
"welcome-paste-clipboard",
225235
]
226-
second_row = ["welcome-exit"]
236+
second_row = ["welcome-connect-database", "welcome-exit"]
227237

228238
try:
229239
# Find currently focused button and its row
@@ -279,6 +289,7 @@ def _activate_focused_button(self) -> None:
279289
"welcome-load-dataset",
280290
"welcome-load-sample",
281291
"welcome-paste-clipboard",
292+
"welcome-connect-database",
282293
"welcome-exit",
283294
]
284295

@@ -292,6 +303,44 @@ def _activate_focused_button(self) -> None:
292303
except Exception as e:
293304
self.log(f"Error activating focused button: {e}")
294305

306+
def _handle_database_connection(self, connection_result: dict | None) -> None:
307+
"""Handle the result from the database connection modal."""
308+
self.log(f"Database connection modal callback called with result: {connection_result}")
309+
310+
if connection_result:
311+
self.log(f"Database connection requested with: {connection_result}")
312+
313+
# Find the data grid and connect to the database
314+
try:
315+
self.log(
316+
f"Looking for data grid, parent: {type(self.parent)}, parent.parent: {type(self.parent.parent) if self.parent else 'None'}"
317+
)
318+
data_grid = self.parent.parent
319+
self.log(f"Found data grid candidate: {type(data_grid)}")
320+
321+
if isinstance(data_grid, ExcelDataGrid):
322+
self.log("Data grid is ExcelDataGrid, proceeding with connection")
323+
if connection_result.get("connection_string"):
324+
connection_string = connection_result["connection_string"]
325+
self.log(f"Calling connect_to_database with: {connection_string}")
326+
data_grid.connect_to_database(connection_string)
327+
# Hide the welcome overlay after successful connection
328+
self.log("Hiding welcome overlay")
329+
self.add_class("hidden")
330+
else:
331+
self.log("No connection string provided in result")
332+
else:
333+
self.log(
334+
f"Data grid not found or wrong type, parent.parent is: {type(data_grid)}"
335+
)
336+
except Exception as e:
337+
self.log(f"Error connecting to database: {e}")
338+
import traceback
339+
340+
self.log(f"Traceback: {traceback.format_exc()}")
341+
else:
342+
self.log("Database connection cancelled or no result")
343+
295344

296345
class DataDirectoryTree(DirectoryTree):
297346
"""A DirectoryTree that filters to show only data files and directories."""
@@ -11289,6 +11338,232 @@ def on_key(self, event) -> None:
1128911338
self.dismiss(None)
1129011339

1129111340

11341+
class DatabaseConnectionModal(ModalScreen[dict | None]):
11342+
"""Modal for connecting to a database."""
11343+
11344+
DEFAULT_CSS = """
11345+
DatabaseConnectionModal {
11346+
align: center middle;
11347+
}
11348+
11349+
DatabaseConnectionModal > Vertical {
11350+
width: 80;
11351+
height: auto;
11352+
max-height: 30;
11353+
padding: 1;
11354+
border: thick $surface;
11355+
background: $surface;
11356+
}
11357+
11358+
DatabaseConnectionModal Label {
11359+
text-align: center;
11360+
padding-bottom: 1;
11361+
color: $text;
11362+
}
11363+
11364+
DatabaseConnectionModal .field-label {
11365+
text-align: left;
11366+
padding-bottom: 0;
11367+
margin-top: 1;
11368+
color: $text;
11369+
}
11370+
11371+
DatabaseConnectionModal Input {
11372+
margin-bottom: 1;
11373+
}
11374+
11375+
DatabaseConnectionModal Select {
11376+
margin-bottom: 1;
11377+
}
11378+
11379+
DatabaseConnectionModal Horizontal {
11380+
height: auto;
11381+
align: center middle;
11382+
}
11383+
11384+
DatabaseConnectionModal Button {
11385+
margin: 0 1;
11386+
min-width: 10;
11387+
}
11388+
11389+
DatabaseConnectionModal TabbedContent {
11390+
height: 1fr;
11391+
}
11392+
11393+
DatabaseConnectionModal TabPane {
11394+
padding: 0;
11395+
}
11396+
11397+
DatabaseConnectionModal VerticalScroll {
11398+
height: 1fr;
11399+
padding: 1;
11400+
}
11401+
"""
11402+
11403+
def compose(self) -> ComposeResult:
11404+
with Vertical():
11405+
yield Label("[bold]Connect to Database[/bold]")
11406+
11407+
with TabbedContent():
11408+
with TabPane("Connection String", id="connection-string-tab"):
11409+
with VerticalScroll():
11410+
yield Label("Enter a complete connection string:", classes="field-label")
11411+
yield Input(
11412+
placeholder="mysql://user:pass@host:port/database",
11413+
id="connection-string-input",
11414+
)
11415+
yield Static("\nExamples:")
11416+
yield Static("• mysql://user:password@host:3306/database")
11417+
yield Static("• postgresql://user:password@host:5432/database")
11418+
11419+
with TabPane("Manual Setup", id="manual-setup-tab"):
11420+
with VerticalScroll():
11421+
yield Label("Database Type:", classes="field-label")
11422+
yield Select(
11423+
[("MySQL", "mysql"), ("PostgreSQL", "postgresql")],
11424+
value="mysql",
11425+
id="db-type-select",
11426+
)
11427+
11428+
yield Label("Host:", classes="field-label")
11429+
yield Input(placeholder="localhost", id="host-input")
11430+
11431+
yield Label("Port:", classes="field-label")
11432+
yield Input(placeholder="3306", id="port-input")
11433+
11434+
yield Label("Database Name:", classes="field-label")
11435+
yield Input(placeholder="database_name", id="database-input")
11436+
11437+
yield Label("Username:", classes="field-label")
11438+
yield Input(placeholder="username", id="username-input")
11439+
11440+
yield Label("Password (optional):", classes="field-label")
11441+
yield Input(placeholder="password", password=True, id="password-input")
11442+
11443+
with Horizontal():
11444+
yield Button("Connect", variant="primary", id="connect-btn")
11445+
yield Button("Cancel", variant="default", id="cancel-btn")
11446+
11447+
def on_mount(self) -> None:
11448+
"""Focus on the connection string input when the modal opens."""
11449+
self.call_after_refresh(self._focus_input)
11450+
11451+
def _focus_input(self) -> None:
11452+
"""Focus on the connection string input."""
11453+
try:
11454+
input_field = self.query_one("#connection-string-input", Input)
11455+
input_field.focus()
11456+
except Exception as e:
11457+
self.log(f"Error focusing input: {e}")
11458+
11459+
def on_button_pressed(self, event: Button.Pressed) -> None:
11460+
"""Handle button presses."""
11461+
self.log(f"DatabaseConnectionModal button pressed: {event.button.id}")
11462+
11463+
if event.button.id == "connect-btn":
11464+
self.log("Connect button pressed, calling _handle_connect")
11465+
self._handle_connect()
11466+
elif event.button.id == "cancel-btn":
11467+
self.log("Cancel button pressed, dismissing modal")
11468+
self.dismiss(None)
11469+
11470+
def _handle_connect(self) -> None:
11471+
"""Handle the connect button press."""
11472+
try:
11473+
self.log("Connect button pressed, handling connection...")
11474+
11475+
# Check which tab is active
11476+
try:
11477+
tabbed_content = self.query_one(TabbedContent)
11478+
active_tab = tabbed_content.active_pane_id
11479+
self.log(f"Active tab: {active_tab}")
11480+
except Exception as e:
11481+
self.log(f"Error getting active tab: {e}")
11482+
# Default to manual setup tab if we can't determine active tab
11483+
active_tab = "manual-setup-tab"
11484+
11485+
if active_tab == "connection-string-tab":
11486+
self.log("Using connection string tab")
11487+
# Use the connection string directly
11488+
try:
11489+
connection_string_input = self.query_one("#connection-string-input", Input)
11490+
connection_string = connection_string_input.value.strip()
11491+
self.log(f"Connection string from input: '{connection_string}'")
11492+
11493+
if not connection_string:
11494+
self.log("No connection string provided")
11495+
# TODO: Show error message to user
11496+
return
11497+
11498+
self.log(f"Dismissing with connection string: {connection_string}")
11499+
self.dismiss({"connection_string": connection_string})
11500+
except Exception as e:
11501+
self.log(f"Error handling connection string tab: {e}")
11502+
return
11503+
11504+
else: # manual-setup-tab or fallback
11505+
self.log("Using manual setup tab")
11506+
# Build connection string from individual fields
11507+
try:
11508+
db_type_select = self.query_one("#db-type-select", Select)
11509+
host_input = self.query_one("#host-input", Input)
11510+
port_input = self.query_one("#port-input", Input)
11511+
database_input = self.query_one("#database-input", Input)
11512+
username_input = self.query_one("#username-input", Input)
11513+
password_input = self.query_one("#password-input", Input)
11514+
11515+
db_type = db_type_select.value
11516+
host = host_input.value.strip() or "localhost"
11517+
port = port_input.value.strip() or ("3306" if db_type == "mysql" else "5432")
11518+
database = database_input.value.strip()
11519+
username = username_input.value.strip()
11520+
password = password_input.value.strip()
11521+
11522+
self.log(
11523+
f"Manual setup values - DB type: {db_type}, Host: {host}, Port: {port}, Database: {database}, Username: {username}, Password: {'***' if password else '(empty)'}"
11524+
)
11525+
11526+
if not database or not username:
11527+
self.log(
11528+
f"Missing required fields - Database: '{database}', Username: '{username}'"
11529+
)
11530+
# TODO: Show error message to user
11531+
return
11532+
11533+
# Build connection string
11534+
if password:
11535+
connection_string = (
11536+
f"{db_type}://{username}:{password}@{host}:{port}/{database}"
11537+
)
11538+
else:
11539+
connection_string = f"{db_type}://{username}@{host}:{port}/{database}"
11540+
11541+
self.log(f"Built connection string: {connection_string}")
11542+
self.log(f"Dismissing with connection string: {connection_string}")
11543+
self.dismiss({"connection_string": connection_string})
11544+
11545+
except Exception as e:
11546+
self.log(f"Error handling manual setup tab: {e}")
11547+
import traceback
11548+
11549+
self.log(f"Traceback: {traceback.format_exc()}")
11550+
return
11551+
11552+
except Exception as e:
11553+
self.log(f"Error handling connect: {e}")
11554+
import traceback
11555+
11556+
self.log(f"Traceback: {traceback.format_exc()}")
11557+
11558+
def on_key(self, event) -> None:
11559+
"""Handle key events for the modal."""
11560+
if event.key == "enter":
11561+
self._handle_connect()
11562+
event.prevent_default()
11563+
elif event.key == "escape":
11564+
self.dismiss(None)
11565+
11566+
1129211567
class SweetFooter(Footer):
1129311568
"""Custom footer with Sweet-specific bindings."""
1129411569

0 commit comments

Comments
 (0)