Skip to content

Commit 1795983

Browse files
author
Bob Strahan
committed
Add "Save as Default" functionality to configuration management
1 parent b45dcdd commit 1795983

File tree

2 files changed

+171
-85
lines changed

2 files changed

+171
-85
lines changed

src/lambda/configuration_resolver/index.py

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,27 @@ def stringify_values(obj):
9494
# Convert everything to string, except None values
9595
return str(obj) if obj is not None else None
9696

97+
def deep_merge(target, source):
98+
"""
99+
Deep merge two dictionaries
100+
"""
101+
result = target.copy()
102+
103+
if not source:
104+
return result
105+
106+
for key, value in source.items():
107+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
108+
result[key] = deep_merge(result[key], value)
109+
else:
110+
result[key] = value
111+
112+
return result
113+
97114
def handle_update_configuration(custom_config):
98115
"""
99116
Handle the updateConfiguration GraphQL mutation
100-
Updates the Custom configuration item in DynamoDB
117+
Updates the Custom or Default configuration item in DynamoDB
101118
"""
102119
try:
103120
# Handle empty configuration case
@@ -117,18 +134,49 @@ def handle_update_configuration(custom_config):
117134
else:
118135
custom_config_obj = custom_config
119136

120-
# Convert all values to strings to ensure compatibility with DynamoDB
121-
stringified_config = stringify_values(custom_config_obj)
137+
# Check if this should be saved as default
138+
save_as_default = custom_config_obj.pop('saveAsDefault', False)
122139

123-
# Update the Custom configuration in DynamoDB
124-
response = table.put_item(
125-
Item={
126-
'Configuration': 'Custom',
127-
**stringified_config
128-
}
129-
)
140+
if save_as_default:
141+
# Get current default configuration
142+
default_item = get_configuration_item('Default')
143+
current_default = remove_configuration_key(default_item) if default_item else {}
144+
145+
# Merge custom changes with current default to create new complete default
146+
new_default_config = deep_merge(current_default, custom_config_obj)
147+
148+
# Convert to strings for DynamoDB
149+
stringified_default = stringify_values(new_default_config)
150+
151+
# Save new default configuration
152+
table.put_item(
153+
Item={
154+
'Configuration': 'Default',
155+
**stringified_default
156+
}
157+
)
158+
159+
# Clear custom configuration
160+
table.put_item(
161+
Item={
162+
'Configuration': 'Custom'
163+
}
164+
)
165+
166+
logger.info(f"Updated Default configuration and cleared Custom: {json.dumps(stringified_default)}")
167+
else:
168+
# Normal custom config update
169+
stringified_config = stringify_values(custom_config_obj)
170+
171+
table.put_item(
172+
Item={
173+
'Configuration': 'Custom',
174+
**stringified_config
175+
}
176+
)
177+
178+
logger.info(f"Updated Custom configuration: {json.dumps(stringified_config)}")
130179

131-
logger.info(f"Updated Custom configuration: {json.dumps(stringified_config)}")
132180
return True
133181

134182
except json.JSONDecodeError as e:

src/ui/src/components/configuration-layout/ConfigurationLayout.jsx

Lines changed: 112 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const ConfigurationLayout = () => {
4242
const [validationErrors, setValidationErrors] = useState([]);
4343
const [viewMode, setViewMode] = useState('form'); // Form view as default
4444
const [showResetModal, setShowResetModal] = useState(false);
45+
const [showSaveAsDefaultModal, setShowSaveAsDefaultModal] = useState(false);
4546

4647
const editorRef = useRef(null);
4748

@@ -473,7 +474,7 @@ const ConfigurationLayout = () => {
473474
}
474475
};
475476

476-
const handleSave = async () => {
477+
const handleSave = async (saveAsDefault = false) => {
477478
// Validate content before saving
478479
const currentErrors = validateCurrentContent();
479480

@@ -580,95 +581,106 @@ const ConfigurationLayout = () => {
580581
return newResult;
581582
};
582583

583-
// Create our customized config by comparing with defaults
584-
const differences = compareWithDefault(formValues, defaultConfig);
584+
let configToSave;
585585

586-
// Flatten path results into a proper object structure - revised to avoid ESLint errors
587-
const buildObjectFromPaths = (paths) => {
588-
// Create a fresh result object
589-
const newResult = {};
590-
591-
Object.entries(paths).forEach(([path, value]) => {
592-
if (!path) return; // Skip empty paths
593-
594-
// For paths with dots, build nested structure
595-
if (path.includes('.') || path.includes('[')) {
596-
// Handle array notation
597-
if (path.includes('[')) {
598-
// Arrays need special handling
599-
// This is simplified - we'll include the whole array when it's customized
600-
const arrayPath = path.split('[')[0];
601-
if (!Object.prototype.hasOwnProperty.call(newResult, arrayPath)) {
602-
// Find the array in formValues
603-
const arrayValue = path.split('.').reduce((acc, part) => {
604-
if (!acc) return undefined;
605-
return acc[part.replace(/\[\d+\]$/, '')];
606-
}, formValues);
607-
608-
if (arrayValue) {
609-
// Create a new object with this property
610-
Object.assign(newResult, { [arrayPath]: arrayValue });
586+
if (saveAsDefault) {
587+
// When saving as default, save the entire current configuration
588+
configToSave = { ...formValues, saveAsDefault: true };
589+
console.log('Saving entire config as new default:', configToSave);
590+
} else {
591+
// Create our customized config by comparing with defaults
592+
const differences = compareWithDefault(formValues, defaultConfig);
593+
594+
// Flatten path results into a proper object structure - revised to avoid ESLint errors
595+
const buildObjectFromPaths = (paths) => {
596+
// Create a fresh result object
597+
const newResult = {};
598+
599+
Object.entries(paths).forEach(([path, value]) => {
600+
if (!path) return; // Skip empty paths
601+
602+
// For paths with dots, build nested structure
603+
if (path.includes('.') || path.includes('[')) {
604+
// Handle array notation
605+
if (path.includes('[')) {
606+
// Arrays need special handling
607+
// This is simplified - we'll include the whole array when it's customized
608+
const arrayPath = path.split('[')[0];
609+
if (!Object.prototype.hasOwnProperty.call(newResult, arrayPath)) {
610+
// Find the array in formValues
611+
const arrayValue = path.split('.').reduce((acc, part) => {
612+
if (!acc) return undefined;
613+
return acc[part.replace(/\[\d+\]$/, '')];
614+
}, formValues);
615+
616+
if (arrayValue) {
617+
// Create a new object with this property
618+
Object.assign(newResult, { [arrayPath]: arrayValue });
619+
}
620+
}
621+
} else {
622+
// Regular object paths
623+
const parts = path.split('.');
624+
625+
// Build an object to merge
626+
const objectToMerge = {};
627+
let current = objectToMerge;
628+
629+
// Build nested structure without modifying existing objects
630+
for (let i = 0; i < parts.length - 1; i += 1) {
631+
// Use += 1 instead of ++
632+
current[parts[i]] = {};
633+
current = current[parts[i]];
611634
}
612-
}
613-
} else {
614-
// Regular object paths
615-
const parts = path.split('.');
616-
617-
// Build an object to merge
618-
const objectToMerge = {};
619-
let current = objectToMerge;
620-
621-
// Build nested structure without modifying existing objects
622-
for (let i = 0; i < parts.length - 1; i += 1) {
623-
// Use += 1 instead of ++
624-
current[parts[i]] = {};
625-
current = current[parts[i]];
626-
}
627635

628-
// Set the value at the final path
629-
current[parts[parts.length - 1]] = value;
636+
// Set the value at the final path
637+
current[parts[parts.length - 1]] = value;
630638

631-
// Deep merge this into result
632-
const deepMerge = (target, source) => {
633-
const output = { ...target };
639+
// Deep merge this into result
640+
const deepMerge = (target, source) => {
641+
const output = { ...target };
634642

635-
Object.keys(source).forEach((key) => {
636-
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
637-
if (target[key] && typeof target[key] === 'object') {
638-
output[key] = deepMerge(target[key], source[key]);
643+
Object.keys(source).forEach((key) => {
644+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
645+
if (target[key] && typeof target[key] === 'object') {
646+
output[key] = deepMerge(target[key], source[key]);
647+
} else {
648+
output[key] = { ...source[key] };
649+
}
639650
} else {
640-
output[key] = { ...source[key] };
651+
output[key] = source[key];
641652
}
642-
} else {
643-
output[key] = source[key];
644-
}
645-
});
653+
});
646654

647-
return output;
648-
};
655+
return output;
656+
};
649657

650-
// Merge into result without modifying original objects
651-
Object.assign(newResult, deepMerge(newResult, objectToMerge));
658+
// Merge into result without modifying original objects
659+
Object.assign(newResult, deepMerge(newResult, objectToMerge));
660+
}
661+
} else {
662+
// For top-level values, create a new object with the property
663+
Object.assign(newResult, { [path]: value });
652664
}
653-
} else {
654-
// For top-level values, create a new object with the property
655-
Object.assign(newResult, { [path]: value });
656-
}
657-
});
658-
659-
return newResult;
660-
};
665+
});
661666

662-
// Convert the difference paths to a proper nested structure
663-
Object.assign(customConfigToSave, buildObjectFromPaths(differences));
667+
return newResult;
668+
};
664669

665-
console.log('Saving customized config:', customConfigToSave);
670+
// Convert the difference paths to a proper nested structure
671+
Object.assign(customConfigToSave, buildObjectFromPaths(differences));
672+
configToSave = customConfigToSave;
673+
console.log('Saving customized config:', configToSave);
674+
}
666675

667676
// Make sure we send at least the Info field, even if no customizations
668-
const success = await updateConfiguration(customConfigToSave);
677+
const success = await updateConfiguration(configToSave);
669678

670679
if (success) {
671680
setSaveSuccess(true);
681+
if (saveAsDefault) {
682+
setShowSaveAsDefaultModal(false);
683+
}
672684
// Force a refresh of the configuration to ensure UI is in sync with backend
673685
setTimeout(() => {
674686
fetchConfiguration();
@@ -810,6 +822,29 @@ const ConfigurationLayout = () => {
810822
</Box>
811823
</Modal>
812824

825+
<Modal
826+
visible={showSaveAsDefaultModal}
827+
onDismiss={() => setShowSaveAsDefaultModal(false)}
828+
header="Save as New Default"
829+
footer={
830+
<Box float="right">
831+
<SpaceBetween direction="horizontal" size="xs">
832+
<Button variant="link" onClick={() => setShowSaveAsDefaultModal(false)}>
833+
Cancel
834+
</Button>
835+
<Button variant="primary" onClick={() => handleSave(true)} loading={isSaving}>
836+
Save as Default
837+
</Button>
838+
</SpaceBetween>
839+
</Box>
840+
}
841+
>
842+
<Box variant="span">
843+
Are you sure you want to save the current configuration as the new default? This will replace the existing
844+
default configuration and cannot be undone.
845+
</Box>
846+
</Modal>
847+
813848
<Container
814849
header={
815850
<Header
@@ -838,7 +873,10 @@ const ConfigurationLayout = () => {
838873
<Button variant="normal" onClick={() => setShowResetModal(true)}>
839874
Restore default (All)
840875
</Button>
841-
<Button variant="primary" onClick={handleSave} loading={isSaving}>
876+
<Button variant="normal" onClick={() => setShowSaveAsDefaultModal(true)}>
877+
Save as default
878+
</Button>
879+
<Button variant="primary" onClick={() => handleSave(false)} loading={isSaving}>
842880
Save changes
843881
</Button>
844882
</SpaceBetween>

0 commit comments

Comments
 (0)