Skip to content

Commit b698416

Browse files
jeremymanningclaude
andcommitted
Fix widget UI issues and add custom filename support
Issues Fixed: - #30: Add proper sizing/scrolling to status output (150px height, scrollable) - #31: Remove inappropriate auto-import message - #32: Add custom filename input for save/load operations Key Changes: - Status output now has fixed height with scroll and border for visibility - Removed print message from load_ipython_extension since widget auto-displays - Added save_filename_input text widget for custom file names - Added file extension validation (.yml, .yaml, .json) - Updated save logic to use custom filename instead of fixed clustrix.yml - Added existing files dropdown for quick selection - Fixed related tests for new behavior Features: - Custom filename: Users can specify any .yml/.yaml/.json filename - File validation: Prevents invalid extensions - Quick selection: Dropdown for existing files fills filename input - Better visibility: Status messages now properly visible with scrolling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b5de5c9 commit b698416

File tree

2 files changed

+54
-22
lines changed

2 files changed

+54
-22
lines changed

clustrix/notebook_magic.py

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,15 @@ def _create_widgets(self):
363363
layout=widgets.Layout(width="auto"),
364364
)
365365
self.delete_btn.on_click(self._on_delete_config)
366-
# Status output
367-
self.status_output = widgets.Output()
366+
# Status output with proper sizing
367+
self.status_output = widgets.Output(
368+
layout=widgets.Layout(
369+
height="150px",
370+
width="100%",
371+
overflow_y="scroll",
372+
border="1px solid #ccc",
373+
)
374+
)
368375
# Update dropdown and load initial configuration
369376
self._update_config_dropdown()
370377
if self.configs:
@@ -554,17 +561,28 @@ def _create_advanced_options(self):
554561
def _create_save_section(self):
555562
"""Create save configuration section."""
556563
style = {"description_width": "120px"}
557-
# File selection dropdown
558-
file_options = ["New file: clustrix.yml"]
564+
565+
# Custom filename input
566+
self.save_filename_input = widgets.Text(
567+
value="clustrix.yml",
568+
description="Filename:",
569+
placeholder="config.yml or config.json",
570+
style=style,
571+
layout=widgets.Layout(width="70%"),
572+
)
573+
574+
# Existing files dropdown (optional)
575+
file_options = ["(Create new file)"]
559576
for config_file in self.config_files:
560-
file_options.append(f"Existing: {config_file}")
577+
file_options.append(f"Overwrite: {config_file}")
561578
self.save_file_dropdown = widgets.Dropdown(
562579
options=file_options,
563580
value=file_options[0],
564-
description="Save to:",
581+
description="Or select:",
565582
style=style,
566583
layout=widgets.Layout(width="70%"),
567584
)
585+
self.save_file_dropdown.observe(self._on_save_file_select, names="value")
568586
# Save button
569587
self.save_btn = widgets.Button(
570588
description="Save Configuration",
@@ -627,6 +645,14 @@ def _setup_change_tracking(self):
627645
for field in fields_to_track:
628646
field.observe(self._mark_unsaved_changes, names="value")
629647

648+
def _on_save_file_select(self, change):
649+
"""Handle selection from existing files dropdown."""
650+
selected = change["new"]
651+
if selected.startswith("Overwrite: "):
652+
# Extract filename from "Overwrite: /path/to/file"
653+
file_path = selected.replace("Overwrite: ", "")
654+
self.save_filename_input.value = str(Path(file_path).name)
655+
630656
def _create_section_containers(self):
631657
"""Create the main UI section containers."""
632658
# Connection fields (dynamically shown/hidden)
@@ -909,13 +935,18 @@ def _on_save_config(self, button):
909935

910936
self.configs[new_config_name] = config
911937
self.current_config_name = new_config_name
912-
# Determine save file
913-
save_option = self.save_file_dropdown.value
914-
if save_option.startswith("New file:"):
915-
save_path = Path("clustrix.yml")
916-
else:
917-
# Extract path from "Existing: /path/to/file"
918-
save_path = Path(save_option.split("Existing: ", 1)[1])
938+
# Determine save file from custom filename input
939+
filename = self.save_filename_input.value.strip()
940+
if not filename:
941+
print("❌ Please enter a filename")
942+
return
943+
944+
# Validate file extension
945+
if not filename.lower().endswith((".yml", ".yaml", ".json")):
946+
print("❌ Filename must end with .yml, .yaml, or .json")
947+
return
948+
949+
save_path = Path(filename)
919950
# Load existing configs if updating a file
920951
if save_path.exists():
921952
existing_configs = load_config_from_file(save_path)
@@ -1071,6 +1102,8 @@ def display(self):
10711102
save_section = widgets.VBox(
10721103
[
10731104
widgets.HTML("<h5>Configuration Management</h5>"),
1105+
widgets.HTML("<h6>Save Configuration</h6>"),
1106+
self.save_filename_input,
10741107
widgets.HBox([self.save_file_dropdown, self.save_btn]),
10751108
widgets.HTML("<br><h6>Load Configuration</h6>"),
10761109
widgets.HBox([self.load_file_upload, self.load_btn]),
@@ -1163,9 +1196,7 @@ def load_ipython_extension(ipython):
11631196
ipython.register_magic_function(
11641197
ClusterfyMagics(ipython).clusterfy, "cell", "clusterfy"
11651198
)
1166-
print(
1167-
"Clustrix notebook magic loaded. Use %%clusterfy to manage configurations."
1168-
)
1199+
# Note: No print message since widget displays automatically on import
11691200

11701201

11711202
# Export the widget class for testing

tests/test_notebook_magic.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -507,12 +507,10 @@ def test_load_ipython_extension(self, mock_ipython_environment):
507507
with patch("clustrix.notebook_magic.ClusterfyMagics") as MockMagics:
508508
mock_magic_instance = MagicMock()
509509
MockMagics.return_value = mock_magic_instance
510-
with patch("builtins.print") as mock_print:
511-
clustrix.notebook_magic.load_ipython_extension(mock_ipython)
512-
MockMagics.assert_called_once_with(mock_ipython)
513-
mock_ipython.register_magic_function.assert_called_once()
514-
mock_print.assert_called_once()
515-
assert "Clustrix notebook magic loaded" in mock_print.call_args[0][0]
510+
clustrix.notebook_magic.load_ipython_extension(mock_ipython)
511+
MockMagics.assert_called_once_with(mock_ipython)
512+
mock_ipython.register_magic_function.assert_called_once()
513+
# Note: No print message expected since widget displays automatically
516514

517515
def test_clusterfy_magic_without_ipython(self):
518516
"""Test magic command fails gracefully without IPython."""
@@ -595,6 +593,9 @@ def test_save_load_cycle(self, mock_ipython_environment):
595593
widget.module_loads.value = ""
596594
widget.pre_exec_commands = MagicMock()
597595
widget.pre_exec_commands.value = ""
596+
# Mock the new filename input field
597+
widget.save_filename_input = MagicMock()
598+
widget.save_filename_input.value = "clustrix.yml"
598599
# Change current directory for the test
599600
import os
600601

0 commit comments

Comments
 (0)