Skip to content

Commit b5de5c9

Browse files
jeremymanningclaude
andcommitted
Implement Load Configuration feature with unsaved changes protection
- Add FileUpload widget for loading .yml/.yaml/.json config files - Implement unsaved changes tracking across all form fields - Add warning system when loading with unsaved changes - Support both single and multiple configuration files - Auto-update dropdown and load first config when multiple found - Clear unsaved changes flag after save/load operations - Two-click confirmation system for loading with unsaved changes Features: - Load single config: Uses config name or filename as key - Load multiple configs: Loads all valid configs, sets first as active - Unsaved changes warning: Protects user from losing current work - File validation: Supports YAML and JSON formats with error handling - UI integration: Added to Configuration Management section Addresses issue #27 - Load Configuration(s) with unsaved changes warning 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ebe1671 commit b5de5c9

File tree

1 file changed

+147
-0
lines changed

1 file changed

+147
-0
lines changed

clustrix/notebook_magic.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ def __init__(self, auto_display: bool = False):
275275
{}
276276
) # Maps config names to their source files
277277
self.auto_display = auto_display
278+
self.has_unsaved_changes = False
278279
# Initialize configurations
279280
self._initialize_configs()
280281
# Create widget components
@@ -573,6 +574,59 @@ def _create_save_section(self):
573574
)
574575
self.save_btn.on_click(self._on_save_config)
575576

577+
# Load configuration widgets
578+
self.load_file_upload = widgets.FileUpload(
579+
accept=".yml,.yaml,.json",
580+
multiple=False,
581+
description="Load Config File",
582+
style=style,
583+
layout=widgets.Layout(width="70%"),
584+
)
585+
self.load_btn = widgets.Button(
586+
description="Load Configuration",
587+
button_style="warning",
588+
icon="upload",
589+
layout=widgets.Layout(width="auto"),
590+
)
591+
self.load_btn.on_click(self._on_load_config)
592+
593+
# Set up change tracking for all form fields
594+
self._setup_change_tracking()
595+
596+
def _mark_unsaved_changes(self, change=None):
597+
"""Mark that there are unsaved changes."""
598+
self.has_unsaved_changes = True
599+
600+
def _clear_unsaved_changes(self):
601+
"""Clear the unsaved changes flag."""
602+
self.has_unsaved_changes = False
603+
604+
def _setup_change_tracking(self):
605+
"""Set up observers to track unsaved changes."""
606+
# Note: config_name already has an observer, but we need to track changes too
607+
# We'll add a second observer for change tracking
608+
fields_to_track = [
609+
self.cluster_type,
610+
self.host_field,
611+
self.username_field,
612+
self.ssh_key_field,
613+
self.port_field,
614+
self.cores_field,
615+
self.memory_field,
616+
self.time_field,
617+
self.k8s_namespace,
618+
self.k8s_image,
619+
self.k8s_remote_checkbox,
620+
self.work_dir_field,
621+
self.package_manager,
622+
self.python_version,
623+
self.env_vars,
624+
self.module_loads,
625+
self.pre_exec_commands,
626+
]
627+
for field in fields_to_track:
628+
field.observe(self._mark_unsaved_changes, names="value")
629+
576630
def _create_section_containers(self):
577631
"""Create the main UI section containers."""
578632
# Connection fields (dynamically shown/hidden)
@@ -676,6 +730,8 @@ def _load_config_to_widgets(self, config_name: str):
676730
self.pre_exec_commands.value = "\n".join(pre_cmds) if pre_cmds else ""
677731
# Update field visibility
678732
self._on_cluster_type_change({"new": self.cluster_type.value})
733+
# Clear unsaved changes flag after loading
734+
self._clear_unsaved_changes()
679735

680736
def _save_config_from_widgets(self) -> Dict[str, Any]:
681737
"""Save current widget values to a configuration dict."""
@@ -892,9 +948,98 @@ def _on_save_config(self, button):
892948
# Update the config dropdown to reflect the new name
893949
self._update_config_dropdown()
894950
self.config_dropdown.value = self.current_config_name
951+
# Clear unsaved changes flag
952+
self._clear_unsaved_changes()
895953
except Exception as e:
896954
print(f"❌ Error saving configuration: {str(e)}")
897955

956+
def _on_load_config(self, button):
957+
"""Load configuration from uploaded file."""
958+
with self.status_output:
959+
self.status_output.clear_output()
960+
961+
# Check if there's a file uploaded
962+
if not self.load_file_upload.value:
963+
print("❌ Please select a configuration file to load")
964+
return
965+
966+
# Check for unsaved changes
967+
if self.has_unsaved_changes:
968+
print("⚠️ Warning: You have unsaved changes!")
969+
print("Current configuration changes will be lost if you continue.")
970+
print("Please save your current configuration first, or:")
971+
print("- Click 'Load Configuration' again to confirm loading")
972+
print("- Use 'Save Configuration' to save current changes")
973+
# Mark as confirmed for next click
974+
if not hasattr(self, "_load_confirmed"):
975+
self._load_confirmed = True
976+
return
977+
else:
978+
# User clicked again, proceed with loading
979+
delattr(self, "_load_confirmed")
980+
981+
try:
982+
# Get the uploaded file
983+
file_info = list(self.load_file_upload.value.values())[0]
984+
file_content = file_info["content"]
985+
file_name = file_info["metadata"]["name"]
986+
987+
# Parse the file content
988+
if file_name.lower().endswith((".yml", ".yaml")):
989+
import yaml
990+
991+
config_data = yaml.safe_load(file_content)
992+
elif file_name.lower().endswith(".json"):
993+
import json
994+
995+
config_data = json.loads(file_content.decode("utf-8"))
996+
else:
997+
print(f"❌ Unsupported file type: {file_name}")
998+
return
999+
1000+
if not isinstance(config_data, dict):
1001+
print(f"❌ Invalid configuration format in {file_name}")
1002+
return
1003+
1004+
# Handle both single config and multiple configs
1005+
configs_loaded = 0
1006+
if "cluster_type" in config_data:
1007+
# Single configuration
1008+
config_name = config_data.get("name", Path(file_name).stem)
1009+
self.configs[config_name] = config_data
1010+
self.current_config_name = config_name
1011+
configs_loaded = 1
1012+
else:
1013+
# Multiple configurations
1014+
for name, config in config_data.items():
1015+
if isinstance(config, dict) and "cluster_type" in config:
1016+
self.configs[name] = config
1017+
configs_loaded += 1
1018+
1019+
# Set the first loaded config as current
1020+
if configs_loaded > 0:
1021+
self.current_config_name = list(config_data.keys())[0]
1022+
1023+
if configs_loaded == 0:
1024+
print(f"❌ No valid configurations found in {file_name}")
1025+
return
1026+
1027+
# Update UI
1028+
self._update_config_dropdown()
1029+
if self.current_config_name:
1030+
self.config_dropdown.value = self.current_config_name
1031+
self._load_config_to_widgets(self.current_config_name)
1032+
1033+
# Clear the file upload
1034+
self.load_file_upload.value = {}
1035+
1036+
print(f"✅ Loaded {configs_loaded} configuration(s) from {file_name}")
1037+
if configs_loaded > 1:
1038+
print(f"Set '{self.current_config_name}' as active configuration")
1039+
1040+
except Exception as e:
1041+
print(f"❌ Error loading configuration: {str(e)}")
1042+
8981043
def display(self):
8991044
"""Display the enhanced widget interface."""
9001045
# Title
@@ -927,6 +1072,8 @@ def display(self):
9271072
[
9281073
widgets.HTML("<h5>Configuration Management</h5>"),
9291074
widgets.HBox([self.save_file_dropdown, self.save_btn]),
1075+
widgets.HTML("<br><h6>Load Configuration</h6>"),
1076+
widgets.HBox([self.load_file_upload, self.load_btn]),
9301077
]
9311078
)
9321079
# Action buttons

0 commit comments

Comments
 (0)