script.js (716 lines of code) (raw):
document.addEventListener('DOMContentLoaded', function () {
// Global variables
let currentData = null;
let selectedMgId = null;
let draggedMgId = null;
let dropSuccessful = false; // Add flag to track successful drops
let isUpdating = false; // Flag to prevent recursive updates
// DOM Elements - add new elements for archetypes
const mgTreeView = document.getElementById('management-groups-tree');
const editForm = document.getElementById('edit-form');
const noSelection = document.getElementById('no-selection');
const mgIdInput = document.getElementById('mg-id');
const mgDisplayNameInput = document.getElementById('mg-display-name');
const mgParentIdSelect = document.getElementById('mg-parent-id');
const mgArchetypesInput = document.getElementById('mg-archetypes-input');
const mgArchetypesTags = document.getElementById('archetypes-tags');
const mgExistsCheckbox = document.getElementById('mg-exists');
const architectureNameInput = document.getElementById('architecture-name');
const jsonPreview = document.getElementById('json-content');
const addArchetypeBtn = document.getElementById('add-archetype-btn');
// Add event listeners for architecture name
architectureNameInput.addEventListener('change', updateArchitectureName);
architectureNameInput.addEventListener('input', handleArchitectureNameInput);
// Add event listeners for real-time management group updates
mgIdInput.addEventListener('input', handleIdUpdate); // Change this line
mgIdInput.addEventListener('blur', validateIdOnBlur); // Rename this function
mgDisplayNameInput.addEventListener('input', handleFieldUpdate);
mgParentIdSelect.addEventListener('change', handleFieldUpdate);
// Replace archetype input listener with new ones
mgArchetypesInput.addEventListener('keydown', handleArchetypeKeydown);
addArchetypeBtn.addEventListener('click', addArchetype);
mgExistsCheckbox.addEventListener('change', handleFieldUpdate);
// Buttons
document.getElementById('new-architecture').addEventListener('click', createNewArchitecture);
document.getElementById('load-alz').addEventListener('click', loadDefaultAlz);
document.getElementById('upload-file').addEventListener('click', () => {
document.getElementById('file-input').click();
});
document.getElementById('file-input').addEventListener('change', handleFileUpload);
document.getElementById('download-file').addEventListener('click', downloadJson);
document.getElementById('add-mg-btn').addEventListener('click', addNewManagementGroup);
document.getElementById('delete-mg-btn').addEventListener('click', deleteManagementGroup);
// Remove save button event listener
// Handle archetype input keydown (to catch Enter key)
function handleArchetypeKeydown(e) {
if (e.key === 'Enter') {
e.preventDefault(); // Prevent form submission
addArchetype();
}
}
// Add an archetype tag
function addArchetype() {
if (!selectedMgId || !currentData) return;
const value = mgArchetypesInput.value.trim();
if (!value) return;
const index = currentData.management_groups.findIndex(mg => mg.id === selectedMgId);
if (index === -1) return;
// Add the archetype if it doesn't already exist
if (!currentData.management_groups[index].archetypes.includes(value)) {
currentData.management_groups[index].archetypes.push(value);
// Update the UI
renderArchetypeTags(currentData.management_groups[index].archetypes);
// Clear the input
mgArchetypesInput.value = '';
// Only update the JSON preview, no need to re-render the entire tree
// which causes the parent dropdown to reset
updateJsonPreview();
}
// Focus back on the input for rapid addition
mgArchetypesInput.focus();
}
// Remove an archetype tag
function removeArchetype(archetype) {
if (!selectedMgId || !currentData) return;
const index = currentData.management_groups.findIndex(mg => mg.id === selectedMgId);
if (index === -1) return;
// Remove the archetype
currentData.management_groups[index].archetypes =
currentData.management_groups[index].archetypes.filter(a => a !== archetype);
// Update the UI
renderArchetypeTags(currentData.management_groups[index].archetypes);
// Only update the JSON preview, no need to re-render the entire tree
updateJsonPreview();
}
// Render archetype tags from array
function renderArchetypeTags(archetypes) {
mgArchetypesTags.innerHTML = '';
if (!archetypes || !archetypes.length) {
return;
}
archetypes.forEach(archetype => {
const tagElement = document.createElement('div');
tagElement.className = 'archetype-tag';
const textElement = document.createElement('span');
textElement.className = 'archetype-tag-text';
textElement.textContent = archetype;
const removeElement = document.createElement('span');
removeElement.className = 'remove-archetype';
removeElement.innerHTML = '×';
removeElement.title = 'Remove this archetype';
removeElement.addEventListener('click', () => removeArchetype(archetype));
tagElement.appendChild(textElement);
tagElement.appendChild(removeElement);
mgArchetypesTags.appendChild(tagElement);
});
}
// Handle updates when any field changes
function handleFieldUpdate(e) {
if (!selectedMgId || !currentData || isUpdating) return;
// Skip handling the ID field, it's handled in handleIdUpdate()
if (e.target === mgIdInput) return;
const index = currentData.management_groups.findIndex(mg => mg.id === selectedMgId);
if (index === -1) return;
// Get current management group
const currentMg = currentData.management_groups[index];
// Get updated values from form
const newDisplayName = mgDisplayNameInput.value.trim();
const newParentId = mgParentIdSelect.value === 'null' ? null : mgParentIdSelect.value;
const newExists = mgExistsCheckbox.checked;
// Handle validation for existence status
if (e.target === mgExistsCheckbox && newExists) {
// Check if parent exists
if (newParentId !== null) {
const parentMg = currentData.management_groups.find(mg => mg.id === newParentId);
if (parentMg && !parentMg.exists) {
alert('A management group cannot exist if its parent is planned. Please set the parent to "exists" first.');
isUpdating = true;
mgExistsCheckbox.checked = false;
isUpdating = false;
return;
}
}
}
if (e.target === mgExistsCheckbox && !newExists) {
// Check if setting this MG to non-existent would conflict with existing children
const hasExistingChildren = currentData.management_groups.some(
mg => mg.parent_id === selectedMgId && mg.exists
);
if (hasExistingChildren) {
alert('Cannot mark this management group as planned when it has existing children. Please update the children first.');
isUpdating = true;
mgExistsCheckbox.checked = true;
isUpdating = false;
return;
}
}
if (e.target === mgParentIdSelect) {
// Check for cycles when changing parent
if (wouldCreateCycle(selectedMgId, newParentId)) {
alert("Cannot move a management group to one of its descendants.");
isUpdating = true;
mgParentIdSelect.value = currentMg.parent_id === null ? 'null' : currentMg.parent_id;
isUpdating = false;
return;
}
// Check if new parent exists when this MG exists
if (currentMg.exists && newParentId !== null) {
const parentMg = currentData.management_groups.find(mg => mg.id === newParentId);
if (parentMg && !parentMg.exists) {
alert('A management group cannot exist if its parent is planned. Please set the parent to "exists" first.');
isUpdating = true;
mgParentIdSelect.value = currentMg.parent_id === null ? 'null' : currentMg.parent_id;
isUpdating = false;
return;
}
}
}
// Update values (removed archetypes as they're now handled separately)
currentMg.display_name = newDisplayName;
currentMg.parent_id = newParentId;
currentMg.exists = newExists;
// Update tree and JSON preview
renderManagementGroups();
updateJsonPreview();
// Re-select the current management group
selectManagementGroup(selectedMgId);
}
// Special handler for ID updates in real-time
function handleIdUpdate(e) {
if (!selectedMgId || !currentData || isUpdating) return;
const newId = mgIdInput.value.trim();
if (!newId) return; // Don't process empty IDs while typing
// If ID hasn't changed, do nothing
if (newId === selectedMgId) return;
// Check if ID already exists
if (currentData.management_groups.some(mg => mg.id === newId && mg.id !== selectedMgId)) {
// Don't show alert while typing - we'll validate fully on blur
return;
}
const index = currentData.management_groups.findIndex(mg => mg.id === selectedMgId);
if (index === -1) return;
// Store the original ID for reference updates
const originalId = selectedMgId;
// Update the management group ID in the data model
currentData.management_groups[index].id = newId;
// Update parent_id references
currentData.management_groups.forEach(mg => {
if (mg.parent_id === originalId) {
mg.parent_id = newId;
}
});
// Update selected ID reference
selectedMgId = newId;
// Update the tree view in real-time, just like for display name changes
// Save cursor position
const cursorPosition = mgIdInput.selectionStart;
// Update the tree view
renderManagementGroups();
// Update JSON preview
updateJsonPreview();
// Select the updated node
const updatedItem = document.querySelector(`.tree-item[data-id="${newId}"]`);
if (updatedItem) {
// Clear previous selection
const prevSelected = document.querySelector('.tree-item.selected');
if (prevSelected) {
prevSelected.classList.remove('selected');
}
updatedItem.classList.add('selected');
}
// Make sure we don't lose focus or cursor position
mgIdInput.focus();
mgIdInput.setSelectionRange(cursorPosition, cursorPosition);
}
// Validate ID when the field loses focus (more comprehensive validation)
function validateIdOnBlur() {
if (!selectedMgId || !currentData) return;
const newId = mgIdInput.value.trim();
// Check for empty ID
if (!newId) {
alert('ID cannot be empty');
// Find the management group with the original ID (it will now have the selectedMgId value)
const currentMg = currentData.management_groups.find(mg => mg.id === selectedMgId);
if (currentMg) {
mgIdInput.value = currentMg.id;
}
return;
}
// Check if ID exists elsewhere in the data model
if (currentData.management_groups.some(mg => mg.id === newId && mg.id !== selectedMgId)) {
alert('A management group with this ID already exists');
// Find the management group with the original ID
const currentMg = currentData.management_groups.find(mg => mg.id === selectedMgId);
if (currentMg) {
mgIdInput.value = currentMg.id;
}
// Reset the tree view to reflect the original ID
renderManagementGroups();
// Reselect the node with the right ID
selectManagementGroup(selectedMgId);
return;
}
// No need to update the tree view again if validation passes,
// as we've been updating it in real-time
}
// Load default ALZ management groups
function loadDefaultAlz() {
currentData = {
"$schema": "https://raw.githubusercontent.com/Azure/Azure-Landing-Zones-Library/main/schemas/architecture_definition.json",
"name": "alz",
"management_groups": [
{
"archetypes": ["root"],
"display_name": "Azure Landing Zones",
"exists": false,
"id": "alz",
"parent_id": null
},
{
"archetypes": ["platform"],
"display_name": "Platform",
"exists": false,
"id": "platform",
"parent_id": "alz"
},
{
"archetypes": ["landing_zones"],
"display_name": "Landing zones",
"exists": false,
"id": "landingzones",
"parent_id": "alz"
},
{
"archetypes": ["corp"],
"display_name": "Corp",
"exists": false,
"id": "corp",
"parent_id": "landingzones"
},
{
"archetypes": ["online"],
"display_name": "Online",
"exists": false,
"id": "online",
"parent_id": "landingzones"
},
{
"archetypes": ["sandbox"],
"display_name": "Sandbox",
"exists": false,
"id": "sandbox",
"parent_id": "alz"
},
{
"archetypes": ["management"],
"display_name": "Management",
"exists": false,
"id": "management",
"parent_id": "platform"
},
{
"archetypes": ["connectivity"],
"display_name": "Connectivity",
"exists": false,
"id": "connectivity",
"parent_id": "platform"
},
{
"archetypes": ["identity"],
"display_name": "Identity",
"exists": false,
"id": "identity",
"parent_id": "platform"
},
{
"archetypes": ["decommissioned"],
"display_name": "Decommissioned",
"exists": false,
"id": "decommissioned",
"parent_id": "alz"
}
]
};
// Update UI
architectureNameInput.value = currentData.name;
document.getElementById('download-file').disabled = false;
renderManagementGroups();
updateJsonPreview();
}
// Create new blank architecture
function createNewArchitecture() {
currentData = {
"$schema": "https://raw.githubusercontent.com/Azure/Azure-Landing-Zones-Library/main/schemas/architecture_definition.json",
"name": "new_architecture",
"management_groups": [
{
"archetypes": ["empty"],
"display_name": "Root Management Group",
"exists": false,
"id": "root-" + Date.now(),
"parent_id": null
}
]
};
renderManagementGroups();
document.getElementById('download-file').disabled = false;
architectureNameInput.value = currentData.name;
updateJsonPreview();
}
// Update the architecture name
function updateArchitectureName() {
if (!currentData) return;
const sanitizedName = sanitizeFilename(architectureNameInput.value.trim() || 'architecture');
currentData.name = sanitizedName;
architectureNameInput.value = sanitizedName;
updateJsonPreview();
}
// Handle real-time updates when typing in architecture name field
function handleArchitectureNameInput() {
if (!currentData) return;
// First validate and sanitize the input
validateFilename();
// Then update the data model and JSON preview
const sanitizedName = architectureNameInput.value;
currentData.name = sanitizedName;
updateJsonPreview();
}
// Validate filename as user types
function validateFilename() {
if (!architectureNameInput.value.trim()) return;
const sanitized = sanitizeFilename(architectureNameInput.value);
if (sanitized !== architectureNameInput.value) {
// Only update if different to avoid cursor jumping
architectureNameInput.value = sanitized;
}
}
// Sanitize a string for use in a filename
function sanitizeFilename(name) {
// Replace invalid filename characters with underscores
// Valid: alphanumeric, underscore, hyphen, and period
return name.replace(/[^a-zA-Z0-9_\-\.]/g, '_');
}
// Handle file upload
function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function (e) {
try {
// Parse JSON
currentData = JSON.parse(e.target.result);
// Validate the JSON structure
if (!currentData.management_groups || !Array.isArray(currentData.management_groups)) {
throw new Error("Invalid JSON format: missing or invalid 'management_groups' array");
}
// Update UI safely
try {
// Set architecture name
architectureNameInput.value = currentData.name || '';
// Enable download button
document.getElementById('download-file').disabled = false;
// Render management groups
renderManagementGroups();
updateJsonPreview();
} catch (renderError) {
console.error('Error rendering data:', renderError);
alert('Error rendering data: ' + renderError.message);
}
} catch (error) {
console.error('Error processing JSON file:', error);
alert('Invalid JSON file: ' + error.message);
// Reset current data if parsing failed
currentData = null;
}
};
reader.readAsText(file);
}
// Download the edited JSON
function downloadJson() {
if (!currentData) return;
// Ensure the architecture name is updated before download
updateArchitectureName();
const jsonString = JSON.stringify(currentData, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const filename = `${currentData.name || 'architecture'}.alz_architecture_definition.json`;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// Build hierarchical tree structure from flat list of management groups
function buildHierarchy(managementGroups) {
const idMap = {};
const root = [];
// Create a map of id to management group
managementGroups.forEach(mg => {
idMap[mg.id] = { ...mg, children: [] };
});
// Build the hierarchy
managementGroups.forEach(mg => {
if (mg.parent_id === null) {
root.push(idMap[mg.id]);
} else if (idMap[mg.parent_id]) {
idMap[mg.parent_id].children.push(idMap[mg.id]);
}
});
return root;
}
// Render management groups as a tree
function renderManagementGroups() {
if (!currentData || !currentData.management_groups) {
mgTreeView.innerHTML = '<p>No data loaded</p>';
return;
}
// Clear the tree view
mgTreeView.innerHTML = '';
// Add legend
addLegend();
// Build the hierarchy
const hierarchy = buildHierarchy(currentData.management_groups);
// Render the tree
hierarchy.forEach(node => {
renderNode(node, mgTreeView);
});
// Update parent select options
updateParentOptions();
}
// Add legend with visual indicators
function addLegend() {
const legendContainer = document.createElement('div');
legendContainer.className = 'legend-container';
// Create "exists" legend item
const existsItem = document.createElement('div');
existsItem.className = 'legend-item';
const existsIndicator = document.createElement('span');
existsIndicator.className = 'legend-indicator exists';
const existsText = document.createElement('span');
existsText.textContent = 'Exists';
existsItem.appendChild(existsIndicator);
existsItem.appendChild(existsText);
// Create "planned" legend item
const plannedItem = document.createElement('div');
plannedItem.className = 'legend-item';
const plannedIndicator = document.createElement('span');
plannedIndicator.className = 'legend-indicator not-exists';
const plannedText = document.createElement('span');
plannedText.textContent = 'Planned';
plannedItem.appendChild(plannedIndicator);
plannedItem.appendChild(plannedText);
// Add items to container
legendContainer.appendChild(existsItem);
legendContainer.appendChild(plannedItem);
// Add legend to tree view
mgTreeView.appendChild(legendContainer);
}
// Determine if a management group can exist based on parent's existence
function canExist(mgId) {
if (!currentData) return false;
const mg = currentData.management_groups.find(mg => mg.id === mgId);
if (!mg) return false;
// Root level management groups can always exist
if (mg.parent_id === null) return true;
// For non-root, check if parent exists
const parent = currentData.management_groups.find(mg => mg.id === mg.parent_id);
return parent ? parent.exists : false;
}
// Get effective existence status (considers parent existence)
function getEffectiveExistenceStatus(mgId) {
if (!currentData) return false;
const mg = currentData.management_groups.find(mg => mg.id === mgId);
if (!mg) return false;
// A management group can only exist if both:
// 1. It's marked as existing in the data model
// 2. Its parent exists (or it's a root management group)
if (!mg.exists) return false;
// Root management groups don't need parent validation
if (mg.parent_id === null) return true;
// Find parent and check its existence
const parent = currentData.management_groups.find(p => p.id === mg.parent_id);
return parent ? parent.exists : false;
}
// Render a single node and its children
function renderNode(node, container) {
const nodeEl = document.createElement('div');
nodeEl.classList.add('tree-item');
// Check effective existence status (considering parent status)
const effectivelyExists = getEffectiveExistenceStatus(node.id);
// Add class based on effective exists property
nodeEl.classList.add(effectivelyExists ? 'mg-exists' : 'mg-not-exists');
nodeEl.dataset.id = node.id;
// If data model and effective status differ, add a special class
if (node.exists && !effectivelyExists) {
nodeEl.classList.add('mg-exists-conflict');
}
// Create drag handle element
const dragHandle = document.createElement('span');
dragHandle.classList.add('drag-handle');
dragHandle.innerHTML = '⋮⋮';
dragHandle.title = 'Drag to reparent';
// Create a status indicator
const statusIndicator = document.createElement('span');
statusIndicator.classList.add('mg-status-indicator');
statusIndicator.title = effectivelyExists ?
'Exists' :
(node.exists ? 'Marked as exists but parent is planned' : 'Does not exist');
// Create text element for the name and ID
const textElement = document.createElement('span');
textElement.classList.add('mg-label');
textElement.textContent = `${node.display_name} (${node.id})`;
// Append the elements in the correct order
nodeEl.appendChild(dragHandle);
nodeEl.appendChild(statusIndicator);
nodeEl.appendChild(textElement);
// Make the element draggable
nodeEl.setAttribute('draggable', 'true');
// Add drag event listeners
nodeEl.addEventListener('dragstart', handleDragStart);
nodeEl.addEventListener('dragover', handleDragOver);
nodeEl.addEventListener('dragenter', handleDragEnter);
nodeEl.addEventListener('dragleave', handleDragLeave);
nodeEl.addEventListener('drop', handleDrop);
nodeEl.addEventListener('dragend', handleDragEnd);
nodeEl.addEventListener('click', (e) => {
e.stopPropagation();
selectManagementGroup(node.id);
});
container.appendChild(nodeEl);
if (node.children && node.children.length > 0) {
const childrenContainer = document.createElement('div');
childrenContainer.classList.add('children');
nodeEl.appendChild(childrenContainer);
node.children.forEach(child => {
renderNode(child, childrenContainer);
});
}
}
// Drag and Drop Event Handlers
function handleDragStart(e) {
// Reset drop status at start of drag
dropSuccessful = false;
// Store the dragged management group ID
draggedMgId = e.target.dataset.id;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', draggedMgId);
e.target.classList.add('dragging');
// Add a delay to prevent immediate drag end on click
setTimeout(() => {
e.target.classList.add('drag-active');
}, 0);
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault(); // Allow drop
}
e.dataTransfer.dropEffect = 'move';
return false;
}
function handleDragEnter(e) {
// Remove drag-over class from all items first
document.querySelectorAll('.tree-item.drag-over').forEach(item => {
item.classList.remove('drag-over');
});
// Find the tree item element (could be a child element that received the event)
const treeItem = e.target.closest('.tree-item');
if (treeItem) {
// Only apply drag-over to the most specific (lowest level) item
// This prevents multiple highlighted ancestors
treeItem.classList.add('drag-over');
// Stop the event from bubbling to prevent parent handlers from firing
e.stopPropagation();
}
}
function handleDragLeave(e) {
// Find the tree item element that triggered the leave event
const treeItem = e.target.closest('.tree-item');
// Only remove the drag-over class if we're actually leaving the element
// and not just moving between its children
if (treeItem && !treeItem.contains(e.relatedTarget)) {
treeItem.classList.remove('drag-over');
}
}
function handleDrop(e) {
e.stopPropagation(); // Stop redirect
e.preventDefault();
// Remove drag-over class from all items
document.querySelectorAll('.tree-item.drag-over').forEach(item => {
item.classList.remove('drag-over');
});
// Find the tree item element (could be a child element that received the event)
const dropTarget = e.target.closest('.tree-item');
if (!dropTarget) {
return false;
}
// Remove highlight from drop target
dropTarget.classList.remove('drag-over');
// Get the target management group ID (where we're dropping)
const dropTargetId = dropTarget.dataset.id;
// Only process if target has an ID (is a management group)
if (!dropTargetId) {
return false;
}
// Don't do anything if dropping onto itself
if (draggedMgId === dropTargetId) {
return false;
}
// Prevent a management group from becoming its own ancestor
if (wouldCreateCycle(draggedMgId, dropTargetId)) {
alert("Cannot move a management group to one of its descendants.");
return false;
}
// Change the parent ID of the dragged management group
updateMgParent(draggedMgId, dropTargetId);
// Mark the drop as successful
dropSuccessful = true;
// Re-render the tree
renderManagementGroups();
// Keep the previously selected management group selected
if (selectedMgId) {
selectManagementGroup(selectedMgId);
}
return false;
}
function handleDragEnd(e) {
// Clean up visual states - ensure all drag-over classes are removed
document.querySelectorAll('.tree-item').forEach(item => {
item.classList.remove('drag-over');
item.classList.remove('dragging');
item.classList.remove('drag-active');
});
// If the drop was not successful (not on a valid target), just leave everything as is
// No need to re-render or modify the data
// Reset drag state
draggedMgId = null;
dropSuccessful = false;
}
// Check if making dropTargetId the parent of draggedMgId would create a cycle
function wouldCreateCycle(draggedId, dropTargetId) {
// Check if drop target is a descendant of dragged item
let currentId = dropTargetId;
const visited = new Set();
while (currentId) {
if (visited.has(currentId)) {
// Cycle detected (shouldn't happen, but just in case)
return true;
}
visited.add(currentId);
if (currentId === draggedId) {
// The drop target is a descendant of the dragged item
return true;
}
// Move up to parent
const parent = currentData.management_groups.find(mg => mg.id === currentId);
currentId = parent ? parent.parent_id : null;
}
return false;
}
// Update the parent of a management group
function updateMgParent(mgId, newParentId) {
const mgIndex = currentData.management_groups.findIndex(mg => mg.id === mgId);
if (mgIndex === -1) return;
// Update the parent_id
currentData.management_groups[mgIndex].parent_id = newParentId;
updateJsonPreview();
}
// Select a management group for editing
function selectManagementGroup(id) {
// Clear previous selection
const prevSelected = document.querySelector('.tree-item.selected');
if (prevSelected) {
prevSelected.classList.remove('selected');
}
// Set new selection
selectedMgId = id;
// Highlight selected item
const selectedItem = document.querySelector(`.tree-item[data-id="${id}"]`);
if (selectedItem) {
selectedItem.classList.add('selected');
}
// Find the management group data
const mg = currentData.management_groups.find(mg => mg.id === id);
if (mg) {
// Set flag to prevent event triggers during form population
isUpdating = true;
// Populate the form
// Don't update ID field if it's currently focused to avoid interrupting typing
if (document.activeElement !== mgIdInput) {
mgIdInput.value = mg.id;
}
mgDisplayNameInput.value = mg.display_name;
mgParentIdSelect.value = mg.parent_id === null ? 'null' : mg.parent_id;
// Clear archetype input and render tags instead
mgArchetypesInput.value = '';
renderArchetypeTags(mg.archetypes);
mgExistsCheckbox.checked = mg.exists;
// Clear flag
isUpdating = false;
// Show the form
noSelection.style.display = 'none';
editForm.style.display = 'block';
}
}
// Add a new management group
function addNewManagementGroup() {
const newMg = {
archetypes: ["empty"],
display_name: "New Management Group",
exists: false,
id: "new-mg-" + Date.now(),
parent_id: null
};
currentData.management_groups.push(newMg);
renderManagementGroups();
selectManagementGroup(newMg.id);
updateJsonPreview();
}
// Delete the selected management group
function deleteManagementGroup() {
if (!selectedMgId || !currentData) return;
// Check if there are any child management groups
const hasChildren = currentData.management_groups.some(mg => mg.parent_id === selectedMgId);
if (hasChildren) {
alert('Cannot delete a management group with children. Please reassign or delete the children first.');
return;
}
// Remove the management group
currentData.management_groups = currentData.management_groups.filter(mg => mg.id !== selectedMgId);
// Reset selection
selectedMgId = null;
noSelection.style.display = 'block';
editForm.style.display = 'none';
renderManagementGroups();
updateJsonPreview();
}
// Update parent select options - with additional error handling
function updateParentOptions() {
if (!mgParentIdSelect) {
console.error('Parent ID select element not found');
return;
}
try {
mgParentIdSelect.innerHTML = '<option value="null">None (Root)</option>';
if (currentData && currentData.management_groups) {
currentData.management_groups.forEach(mg => {
const option = document.createElement('option');
option.value = mg.id;
option.textContent = `${mg.display_name} (${mg.id})`;
mgParentIdSelect.appendChild(option);
});
}
} catch (error) {
console.error('Error updating parent options:', error);
}
}
// Make dropzone cover entire tree view area to prevent dropping outside valid targets
function setupDropzone() {
// Add event listeners to the tree container to catch drops that miss targets
mgTreeView.addEventListener('dragover', function (e) {
e.preventDefault(); // Allow dropping
return false;
});
mgTreeView.addEventListener('drop', function (e) {
// If the drop is not on a management group item, prevent any action
if (!e.target.classList.contains('tree-item') &&
!e.target.closest('.tree-item')) {
e.preventDefault();
e.stopPropagation();
}
});
}
// Update the JSON preview
function updateJsonPreview() {
if (!currentData) {
jsonPreview.innerHTML = 'No data loaded yet.';
return;
}
const jsonString = JSON.stringify(currentData, null, 2)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")(\s*:)?|(\btrue\b|\bfalse\b|\bnull\b)|(-?\d+\.?\d*(?:[eE][+\-]?\d+)?)/g, match => {
let cls = 'json-number';
if (/^"/.test(match)) {
cls = /:$/.test(match) ? 'json-key' : 'json-string';
} else if (/true|false/.test(match)) {
cls = 'json-boolean';
} else if (/null/.test(match)) {
cls = 'json-null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
jsonPreview.innerHTML = jsonString;
}
// JSON syntax highlighting
function syntaxHighlight(json) {
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
let cls = 'json-number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'json-key';
match = match.replace(':', '');
} else {
cls = 'json-string';
}
} else if (/true|false/.test(match)) {
cls = 'json-boolean';
} else if (/null/.test(match)) {
cls = 'json-null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
}
// Add CSS for conflict indication
const style = document.createElement('style');
style.textContent = `
.mg-exists-conflict .mg-status-indicator {
border: 2px solid #dc3545 !important;
position: relative;
}
.mg-exists-conflict .mg-status-indicator::after {
content: "!";
position: absolute;
color: #dc3545;
font-weight: bold;
font-size: 10px;
top: -5px;
right: -5px;
}
`;
document.head.appendChild(style);
// Initialize
renderManagementGroups();
setupDropzone();
updateJsonPreview();
});