Newer
Older
EnvoyControlPlane / static / modals.js
// modals.js
import { setInconsistencyData, inconsistencyData } from './global.js';
import { loadAllData } from './data_loader.js'; // Will be imported later
import { configStore, API_BASE_URL } from './global.js';

// =========================================================================
// UTILITY HANDLERS (Copy & Download)
// =========================================================================

export function copyToClipboard(tabName) {
    let contentToCopy = '';

    // Get content safely
    if (tabName === 'json') {
        const jsonContentElement = document.getElementById('modal-json-content');
        contentToCopy = jsonContentElement?.textContent?.trim() || '';
    } else if (tabName === 'yaml') {
        const yamlContentElement = document.getElementById('modal-yaml-content');
        contentToCopy = yamlContentElement?.textContent?.trim() || '';
    }

    if (!contentToCopy) {
        alert(`No ${tabName.toUpperCase()} content found to copy.`);
        return;
    }

    // Clipboard API
    navigator.clipboard.writeText(contentToCopy)
        .then(() => {
            const copyButton = document.getElementById('copy-config-button');
            if (copyButton) {
                // Instead of replacing innerHTML (which may destroy icons/styles),
                // we temporarily change text via a data attribute or title
                const originalLabel = copyButton.dataset.label || copyButton.textContent;
                copyButton.dataset.label = originalLabel;

                copyButton.classList.add('copied'); // optional styling via CSS
                copyButton.textContent = '✅ Copied!';
                setTimeout(() => {
                    copyButton.textContent = originalLabel;
                    copyButton.classList.remove('copied');
                }, 1500);
            } else {
                alert('Configuration copied to clipboard!');
            }
        })
        .catch(err => {
            console.error('Failed to copy text: ', err);
            alert('Failed to copy text. Please try again or check browser permissions.');
        });
}

/**
 * Downloads the YAML configuration content.
 */
export function downloadYaml() {
    const yamlContent = document.getElementById('modal-yaml-content').textContent;
    if (!yamlContent || yamlContent.trim() === '') {
        alert("No YAML content available to download.");
        return;
    }

    // Use modal title as filename fallback
    const title = document.getElementById('modal-title').textContent
        .replace(/\s+/g, '_')
        .replace(/[^\w\-]/g, '');
    
    const blob = new Blob([yamlContent], { type: 'text/yaml' });
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = url;
    a.download = title ? `${title}.yaml` : 'config.yaml';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
}

// =========================================================================
// GENERIC MODAL HANDLERS
// =========================================================================

/**
 * Shows any modal element by its ID. (NEW GENERIC FUNCTION)
 * @param {string} modalId - The ID of the modal element to display.
 */
export function showModal(modalId) {
    const modal = document.getElementById(modalId);
    if (modal) {
        modal.style.display = 'block';
    }
}

/**
 * Hides any modal element by its ID. (NEW GENERIC FUNCTION)
 * @param {string} modalId - The ID of the modal element to hide. Defaults to 'configModal'.
 */
export function hideModal(modalId = 'configModal') {
    const modal = document.getElementById(modalId);
    modal?.querySelector('.modal-content')?.querySelector(".tab-controls")?.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
    if (modal) {
        modal.style.display = 'none';
    }
}


// =========================================================================
// CONFIGURATION DISPLAY MODAL HANDLERS (JSON/YAML tabs)
// =========================================================================

export function switchTab(modalContent, tabName) {
    // Deactivate all tab buttons
    const tabSelection = modalContent.querySelector('.tab-selection');
    if (tabSelection) {
        tabSelection.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
    }

    // Hide all code blocks
    modalContent.querySelectorAll('.code-block').forEach(content => {
        content.style.display = 'none';
    });

    // Activate selected tab button and content
    const activeBtn = modalContent.querySelector(`.tab-button[data-tab="${tabName}"]`);
    const activeContent = document.getElementById(`modal-${tabName}-content`);
    if (activeBtn) activeBtn.classList.add('active');
    if (activeContent) activeContent.style.display = 'block';

    // Ensure copy button copies from the correct tab
    const copyButton = document.getElementById('copy-config-button');
    if (copyButton) {
        // Remove any existing event listener safely
        const newHandler = () => copyToClipboard(tabName);
        copyButton.replaceWith(copyButton.cloneNode(true)); // clone to remove old listeners
        const newCopyButton = document.getElementById('copy-config-button');
        newCopyButton.addEventListener('click', newHandler);
    }
}


/**
 * Displays the main configuration modal (with JSON/YAML tabs). (RENAMED FROM showModal)
 * @param {string} title - The modal title.
 * @param {object} jsonData - The configuration data object (for JSON tab).
 * @param {string} yamlData - The configuration data as a YAML string.
 * @param {string} [defaultTab='yaml'] - The tab to show by default.
 */
export function showConfigModal(title, jsonData, yamlData, defaultTab = 'yaml') {
    document.getElementById('modal-title').textContent = title;
    
    // 1. Check if the 'yaml' key exists in the jsonData object.
    // The "yaml" attribute is only used for storing yaml context, when we display it in the frontent,
    // we should exclude it from the display to confuse the user.
    if (jsonData && jsonData.yaml !== undefined) {
        // 2. Use the delete operator to remove the key
        delete jsonData.yaml;
    }

    // Populate JSON content
    document.getElementById('modal-json-content').textContent =
        JSON.stringify(jsonData, null, 2);

    // Populate YAML content
    document.getElementById('modal-yaml-content').textContent = yamlData;
    
    // Default to the specified tab
    const modalContent = document.getElementById('configModal')?.querySelector('.modal-content');
    if (modalContent) {
        // This call to switchTab will also set the initial handler for the copy button
        switchTab(modalContent, defaultTab); 
    }

    // Use generic showModal
    showModal('configModal');
}

/**
 * Hides the main configuration modal (configModal). (Uses generic hideModal)
 */
export function hideConfigModal() {
    hideModal('configModal');
}

/**
 * Sets up click handlers for the tab buttons in the main modal.
 */
export function setupModalTabs() {
    const modalContent = document.getElementById('configModal')?.querySelector('.modal-content');
    if (!modalContent) return;

    modalContent.querySelectorAll('.tab-button').forEach(button => {
        button.addEventListener('click', (event) => {
            const tabName = event.target.getAttribute('data-tab');
            // The switchTab function now includes the logic to update the copy button's target
            switchTab(modalContent, tabName); 
        });
    });
}

// =========================================================================
// ADD FILTER CHAIN MODAL HANDLERS
// =========================================================================

/**
 * Shows the modal for adding a new filter chain to a listener.
 * @param {string} listenerName - The name of the listener to modify.
 */
export function showAddFilterChainModal(listenerName) {
    // 1. Set the listener name in the hidden input for form submission
    document.getElementById('add-fc-listener-name').value = listenerName;
    
    // 2. Set the title
    document.getElementById('add-fc-modal-title').textContent = 
        `Add New Filter Chain to: ${listenerName}`;
        
    // 3. Clear any previous YAML content
    const yamlInput = document.getElementById('add-fc-yaml-input');
    yamlInput.value = '';
    
    // 4. Show the modal (using generic showModal)
    showModal('addFilterChainModal');
    
    // 5. Provide a template to guide the user (optional)
    yamlInput.placeholder = 
`# Paste your new Filter Chain YAML here.
# NOTE: The root key should be the filter chain object itself.
filter_chain_match:
  server_names: ["new.example.com"]
...`;
}

/**
 * Closes the Add Filter Chain modal. (Uses generic hideModal)
 */
export function hideAddFilterChainModal() {
    hideModal('addFilterChainModal');
}


// =========================================================================
// CONSISTENCY MODAL HANDLERS
// =========================================================================

export function showConsistencyModal() {
    if (!inconsistencyData || inconsistencyData.inconsistent === false) return;

    // Populate modal content
    const cacheOnly = inconsistencyData['cache-only'] || {};
    const dbOnly = inconsistencyData['db-only'] || {};
    
    document.getElementById('cache-only-count').textContent = 
        Object.keys(cacheOnly).length;
    document.getElementById('cache-only-data').textContent = 
        JSON.stringify(cacheOnly, null, 2);

    document.getElementById('db-only-count').textContent = 
        Object.keys(dbOnly).length;
    document.getElementById('db-only-data').textContent = 
        JSON.stringify(dbOnly, null, 2);
    
    // Use generic showModal
    showModal('consistencyModal');
}

export function hideConsistencyModal() {
    hideModal('consistencyModal');
}

// =========================================================================
// ADD CLUSTER MODAL HANDLERS (Placeholder, assuming definitions are elsewhere)
// =========================================================================

// Assuming these are defined elsewhere and attached to window, 
// or are intended to be moved here:
// export function showAddClusterModal() { showModal('addClusterModal'); }
// export function hideAddClusterModal() { hideModal('addClusterModal'); }


// =========================================================================
// WINDOW EVENT LISTENERS
// =========================================================================

window.addEventListener('keydown', (event) => {
    // Check for Escape key to close all modals
    if (event.key === 'Escape') {
        hideConfigModal();
        hideAddFilterChainModal();
        hideConsistencyModal();
        // Assuming other hideModal functions (like hideAddClusterModal) are either imported or globally attached.
        // For example:
        // window.hideAddClusterModal?.();
        // window.hideAddListenerModal?.();
        // window.hideAddSecretModal?.();
    }
});

// Close modal when clicking outside of the content (on the backdrop)
window.addEventListener('click', (event) => {
    const modal = document.getElementById('configModal');
    const addFCModal = document.getElementById('addFilterChainModal'); 
    const consistencyModal = document.getElementById('consistencyModal');
    const addListenerModal = document.getElementById('addListenerModal'); 
    const addClusterModal = document.getElementById('addClusterModal');

    if (event.target === modal) {
        hideConfigModal();
    }
    if (event.target === addFCModal) {
        hideAddFilterChainModal();
    }
    if (event.target === consistencyModal) {
        hideConsistencyModal();
    }
    if (event.target === addListenerModal) {
        // Call global function if defined externally
        // window.hideAddListenerModal?.();
        // Or hide using generic function if it belongs here:
        hideModal('addListenerModal'); 
    }
    if (event.target === addClusterModal) {
        // Call global function if defined externally
        // window.hideAddClusterModal?.();
        // Or hide using generic function if it belongs here:
        hideModal('addClusterModal');
    }
});

// =========================================================================
// CONFIG-SPECIFIC MODAL LAUNCHERS (Fetch & Display)
// =========================================================================

/**
 * Fetches and displays the full configuration for a listener in the tabbed modal.
 */
export async function showListenerConfigModal(listenerName) {
    const config = configStore.listeners[listenerName];
    if (!config) {
        showConfigModal(`🚨 Error: Listener Not Found`, { name: listenerName, error: 'Configuration data missing from memory.' }, 'Error: Listener not in memory.');
        return;
    }

    let yamlData = configStore.listeners[listenerName]?.yaml || 'Loading YAML...';
    
    if (yamlData === 'Loading YAML...') {
        try {
            const response = await fetch(`${API_BASE_URL}/get-listener?name=${listenerName}&format=yaml`);
            if (!response.ok) {
                yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`;
            } else {
                yamlData = await response.text();
                configStore.listeners[listenerName].yaml = yamlData; // Store YAML
            }
        } catch (error) {
            console.error("Failed to fetch YAML listener config:", error);
            yamlData = `Network Error fetching YAML: ${error.message}`;
        }
    }

    // Use the renamed function
    showConfigModal(`Full Config for Listener: ${listenerName}`, config, yamlData);
}


/**
 * Fetches and displays the full configuration for a secret in the tabbed modal.
 */
export async function showSecretConfigModal(secretName) {
         const config = configStore.secrets[secretName];
    if (!config) {
        showConfigModal(`🚨 Error: Secret Not Found`, { name: secretName, error: 'Configuration data missing from memory.' }, 'Error: Secret not in memory.');
        return;
    }

    let yamlData = configStore.secrets[secretName]?.yaml || 'Loading YAML...';
    
    if (yamlData === 'Loading YAML...') {
        try {
            const response = await fetch(`${API_BASE_URL}/get-secret?name=${secretName}&format=yaml`);
            if (!response.ok) {
                yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`;
            } else {
                yamlData = await response.text();
                configStore.secrets[secretName].yaml = yamlData; // Store YAML
            }
        } catch (error) {
            console.error("Failed to fetch YAML listener config:", error);
            yamlData = `Network Error fetching YAML: ${error.message}`;
        }
    }

    // Use the renamed function
    showConfigModal(`Full Config for Secret: ${secretName}`, config, yamlData);
}

/**
 * Fetches and displays the full configuration for a cluster in the tabbed modal.
 */
export async function showClusterConfigModal(clusterName) {
    const config = configStore.clusters[clusterName];
    if (!config) {
        showConfigModal(`🚨 Error: Cluster Not Found`, { name: clusterName, error: 'Configuration data missing from memory.' }, 'Error: Cluster not in memory.');
        return;
    }

    let yamlData = configStore.clusters[clusterName]?.yaml || 'Loading YAML...';
    
    if (yamlData === 'Loading YAML...') {
        try {
            const response = await fetch(`${API_BASE_URL}/get-cluster?name=${clusterName}&format=yaml`);
            if (!response.ok) {
                yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`;
            } else {
                yamlData = await response.text();
                configStore.clusters[clusterName].yaml = yamlData; // Store YAML
            }
        } catch (error) {
            console.error("Failed to fetch YAML cluster config:", error);
            yamlData = `Network Error fetching YAML: ${error.message}`;
        }
    }

    showConfigModal(`Full Config for Cluster: ${clusterName}`, config, yamlData);
}

// =========================================================================
// NEW: EXTENSION CONFIG MODAL HANDLER
// =========================================================================

/**
 * Fetches and displays the full configuration for an ExtensionConfig in the tabbed modal.
 */
export async function showExtensionConfigModal(configName) {
    const config = configStore.extension_configs[configName];
    if (!config) {
        showConfigModal(`🚨 Error: ExtensionConfig Not Found`, { name: configName, error: 'Configuration data missing from memory.' }, 'Error: ExtensionConfig not in memory.');
        return;
    }

    let yamlData = configStore.extension_configs[configName]?.yaml || 'Loading YAML...';
    
    if (yamlData === 'Loading YAML...') {
        try {
            const response = await fetch(`${API_BASE_URL}/get-extensionconfig?name=${configName}&format=yaml`);
            if (!response.ok) {
                yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`;
            } else {
                yamlData = await response.text();
                configStore.extension_configs[configName].yaml = yamlData; // Store YAML
            }
        } catch (error) {
            console.error("Failed to fetch YAML ExtensionConfig:", error);
            yamlData = `Network Error fetching YAML: ${error.message}`;
        }
    }

    showConfigModal(`Full Config for ExtensionConfig: ${configName}`, config, yamlData);
}