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
296345class 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 ("\n Examples:" )
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+
1129211567class SweetFooter (Footer ):
1129311568 """Custom footer with Sweet-specific bindings."""
1129411569
0 commit comments