@@ -275,6 +275,7 @@ def __init__(self, auto_display: bool = False):
275
275
{}
276
276
) # Maps config names to their source files
277
277
self .auto_display = auto_display
278
+ self .has_unsaved_changes = False
278
279
# Initialize configurations
279
280
self ._initialize_configs ()
280
281
# Create widget components
@@ -573,6 +574,59 @@ def _create_save_section(self):
573
574
)
574
575
self .save_btn .on_click (self ._on_save_config )
575
576
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
+
576
630
def _create_section_containers (self ):
577
631
"""Create the main UI section containers."""
578
632
# Connection fields (dynamically shown/hidden)
@@ -676,6 +730,8 @@ def _load_config_to_widgets(self, config_name: str):
676
730
self .pre_exec_commands .value = "\n " .join (pre_cmds ) if pre_cmds else ""
677
731
# Update field visibility
678
732
self ._on_cluster_type_change ({"new" : self .cluster_type .value })
733
+ # Clear unsaved changes flag after loading
734
+ self ._clear_unsaved_changes ()
679
735
680
736
def _save_config_from_widgets (self ) -> Dict [str , Any ]:
681
737
"""Save current widget values to a configuration dict."""
@@ -892,9 +948,98 @@ def _on_save_config(self, button):
892
948
# Update the config dropdown to reflect the new name
893
949
self ._update_config_dropdown ()
894
950
self .config_dropdown .value = self .current_config_name
951
+ # Clear unsaved changes flag
952
+ self ._clear_unsaved_changes ()
895
953
except Exception as e :
896
954
print (f"❌ Error saving configuration: { str (e )} " )
897
955
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
+
898
1043
def display (self ):
899
1044
"""Display the enhanced widget interface."""
900
1045
# Title
@@ -927,6 +1072,8 @@ def display(self):
927
1072
[
928
1073
widgets .HTML ("<h5>Configuration Management</h5>" ),
929
1074
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 ]),
930
1077
]
931
1078
)
932
1079
# Action buttons
0 commit comments