Newer
Older
EnvoyControlPlane / static / global.js
// global.js
// Use 'export let' to make it available for other modules
export let API_BASE_URL = window.location.href;
API_BASE_URL = API_BASE_URL.replace(/\/$/, ""); 
// Note: When reassigning, you don't repeat 'export'

// =========================================================================
// GLOBAL IN-MEMORY STORE
// =========================================================================
// This object will hold the full configuration data in JavaScript memory.
// It stores JSON configs and caches the raw YAML string under the 'yaml' key.
export const configStore = {
    clusters: {},
    listeners: {}
    // listener objects will now have a 'filterChains' array to store domain configs
};

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

/**
 * Displays the modal with JSON and YAML content.
 * @param {string} title - The modal title.
 * @param {object} jsonData - The configuration data object (used for JSON tab).
 * @param {string} yamlData - The configuration data as a YAML string.
 * @param {string} defaultTab - The tab to show by default ('json' or 'yaml').
 */
export function showConfigModal(title, jsonData, yamlData, defaultTab = 'yaml') {
    document.getElementById('modal-title').textContent = title;
    
    // 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) {
        switchTab(modalContent, defaultTab); 
    }

    document.getElementById('configModal').style.display = 'block';
}

export function hideModal() {
    document.getElementById('configModal').style.display = 'none';
}

window.addEventListener('keydown', (event) => {
    // Check for Escape key to close all modals
    if (event.key === 'Escape') {
        hideModal();
        window.hideAddFilterChainModal?.(); 
        window.hideAddListenerModal?.();
        window.hideAddClusterModal?.();
    }
});

// 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 addListenerModal = document.getElementById('addListenerModal');
    const addClusterModal = document.getElementById('addClusterModal');
    
    if (event.target === modal) {
        hideModal();
    }
    if (event.target === addFCModal) {
        window.hideAddFilterChainModal?.();
    }
    if (event.target === addListenerModal) {
        window.hideAddListenerModal?.();
    }
    if (event.target === addClusterModal) {
        window.hideAddClusterModal?.();
    }
});


// Helper function that MUST be in your HTML/JS setup for the tabs to work
function switchTab(modalContent, tabName) {
    // Deactivate all buttons and hide all content
    modalContent.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
    modalContent.querySelectorAll('.code-block').forEach(content => content.style.display = 'none');

    // Activate the selected button and show the corresponding 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';
}

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');
            switchTab(modalContent, tabName);
        });
    });
}

// // =========================================================================
// // CONFIG-SPECIFIC MODAL LAUNCHERS 
// // =========================================================================

// /**
//  * Handles showing the configuration for an individual FilterChain/Domain.
//  */
// export async function showDomainConfig(element) {
//     const title = element.getAttribute('data-title');
//     const listenerName = element.getAttribute('data-listener-name');
//     const chainIndex = element.getAttribute('data-chain-index'); 

//     if (!listenerName || chainIndex === null) {
//         console.error("Missing required data attributes for domain config.");
//         return;
//     }
    
//     const listener = configStore.listeners[listenerName];
//     const jsonData = listener?.filterChains?.[parseInt(chainIndex)];

//     if (!jsonData) {
//         const errorMsg = 'Filter Chain configuration not found in memory.';
//         console.error(errorMsg);
//         showConfigModal(`🚨 Error: ${title}`, { error: errorMsg }, errorMsg);
//         return;
//     }

//     let yamlData = 'Generating YAML from in-memory JSON...';
//     let defaultTab = 'json';
    
//     try {
//         if (typeof require === 'undefined' && typeof jsyaml === 'undefined') {
//             throw new Error("YAML parser (e.g., js-yaml) is required but not found.");
//         }
        
//         const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml;

//         yamlData = yaml.dump(jsonData, {
//             indent: 2, 
//             lineWidth: -1, 
//             flowLevel: -1 
//         });
//         defaultTab = 'yaml'; 
        
//     } catch (error) {
//         console.error("Failed to generate YAML from JSON. Falling back to approximation.", error);
        
//         const yamlApproximation = JSON.stringify(jsonData, null, 2)
//             .replace(/[{}]/g, '')
//             .replace(/"(\w+)":\s*/g, '$1: ')
//             .replace(/,\n\s*/g, '\n')
//             .replace(/\[\n\s*(\s*)/g, '\n$1  - ')
//             .replace(/,\n\s*(\s*)/g, '\n$1- ')
//             .replace(/:\s*"/g, ': ')
//             .replace(/"/g, '');
            
//         yamlData = yamlApproximation + `\n\n--- WARNING: YAML is an approximation because the js-yaml library is missing or failed to parse. ---\n\n`;
//         defaultTab = 'json'; 
//     }
    
//     showConfigModal(title, jsonData, yamlData, defaultTab);
// }


/**
 * Handles showing the full configuration for a Cluster. (REMAINS HERE as a launcher)
 */
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);
}

/**
 * Handles showing the full configuration for a Listener.
 */
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}`;
        }
    }

    showConfigModal(`Full Config for Listener: ${listenerName}`, config, yamlData);
}


// =========================================================================
// FILTER CHAIN ADDITION LOGIC 
// =========================================================================

/**
 * Shows the modal for adding a new filter chain to a listener.
 */
export function showAddFilterChainModal(listenerName) {
    document.getElementById('add-fc-listener-name').value = listenerName;
    document.getElementById('add-fc-modal-title').textContent = 
        `Add New Filter Chain to: ${listenerName}`;
    const yamlInput = document.getElementById('add-fc-yaml-input');
    yamlInput.value = '';
    document.getElementById('addFilterChainModal').style.display = 'block';
    
    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"]
filters:
  - name: envoy.filters.network.http_connection_manager
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
      stat_prefix: new_route_http
      route_config:
        virtual_hosts:
        - name: new_service
          domains: ["new.example.com"]
          routes:
          - match: { prefix: "/" }
            route: { cluster: "new_backend_cluster" }
      http_filters:
      - name: envoy.filters.http.router
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
`;
}

/**
 * Handles the submission of the new filter chain YAML.
 */
export async function submitNewFilterChain() {
    const listenerName = document.getElementById('add-fc-listener-name').value;
    const yamlData = document.getElementById('add-fc-yaml-input').value.trim();

    if (!yamlData) {
        alert("Please paste the filter chain YAML configuration.");
        return;
    }
    
    if (!listenerName) {
        alert("Listener name is missing. Cannot submit.");
        return;
    }

    const payload = { listener_name: listenerName, yaml: yamlData };

    try {
        const response = await fetch(`${API_BASE_URL}/append-filter-chain`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });

        if (!response.ok) {
            const errorBody = await response.text();
            throw new Error(`HTTP Error ${response.status}: ${errorBody}`);
        }

        alert(`Successfully appended new filter chain to '${listenerName}'.`);
        
        document.getElementById('addFilterChainModal').style.display = 'none';

        cleanupConfigStore(); 
        window.listListeners?.();
    } catch (error) {
        console.error(`Failed to append filter chain to '${listenerName}':`, error);
        alert(`Failed to append filter chain. Check console for details. Error: ${error.message}`);
    }
}

/**
 * Closes the Add Filter Chain modal.
 */
export function hideAddFilterChainModal() {
    document.getElementById('addFilterChainModal').style.display = 'none';
}


// =========================================================================
// CLUSTER LOGIC (REMOVED - ONLY REFERENCES LEFT)
// =========================================================================
// listClusters() logic is now imported and exposed via window.listClusters

// export function disableCluster(clusterName, event) {
//     event.stopPropagation();
//     window.disableCluster(clusterName, event);
// }

// export function enableCluster(clusterName, event) {
//     event.stopPropagation();
//     window.enableCluster(clusterName, event);
// }

// export function removeCluster(clusterName, event) {
//     event.stopPropagation();
//     window.removeCluster(clusterName, event);
// }

// =========================================================================
// CONSISTENCY LOGIC (REMAINS HERE)
// =========================================================================

/**
 * Cleans up the cached YAML data in configStore for all clusters and listeners.
 */
export function cleanupConfigStore() {
    for (const name in configStore.clusters) {
        if (configStore.clusters.hasOwnProperty(name)) {
            configStore.clusters[name].yaml = 'Loading YAML...';
        }
    }

    for (const name in configStore.listeners) {
        if (configStore.listeners.hasOwnProperty(name)) {
            configStore.listeners[name].yaml = 'Loading YAML...';
        }
    }
}


export const CONSISTENCY_POLL_INTERVAL = 5000; 
export let inconsistencyData = null; 

export function setInconsistencyData(data) {
    inconsistencyData = data;
}

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

    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);
    
    document.getElementById('consistencyModal').style.display = 'block';
}

export function hideConsistencyModal() {
    document.getElementById('consistencyModal').style.display = 'none';
}

export async function checkConsistency() {
    const button = document.getElementById('consistency-button');
    if (!button) return;

    const hasPreviousConflict = inconsistencyData !== null;
    const isCurrentlyError = button.classList.contains('error');

    button.textContent = 'Checking...';
    button.classList.add('loading');
    button.classList.remove('consistent', 'inconsistent', 'error');

    try {
        const response = await fetch(`${API_BASE_URL}/is-consistent`);
        if (!response.ok) throw new Error(response.statusText);

        const data = await response.json();
        const consistencyStatus = data.consistent;
        const isNewConflict = consistencyStatus.inconsistent === true;
        const stateChanged = isNewConflict !== hasPreviousConflict;

        button.classList.remove('loading');

        if (isNewConflict) {
            button.textContent = '🚨 CONFLICT';
            if (stateChanged || isCurrentlyError) {
                button.classList.remove('consistent', 'error');
                button.classList.add('inconsistent');
                button.disabled = false;
                inconsistencyData = consistencyStatus;
            }
        } else {
            button.textContent = '✅ Consistent';
            if (stateChanged || isCurrentlyError) {
                button.classList.remove('inconsistent', 'error');
                button.classList.add('consistent');
                button.disabled = true;
                inconsistencyData = null;
                hideConsistencyModal();
            }
        }

    } catch (error) {
        button.classList.remove('loading', 'consistent', 'inconsistent');
        button.classList.add('error');
        button.textContent = '❌ Error';
        button.disabled = true;
        inconsistencyData = null;
        console.error("Consistency check failed:", error);
    }
}

/**
 * Core function to resolve consistency by making a POST call to a sync endpoint.
 */
async function resolveConsistency(action) {
    let url = (action === 'flush') ? `${API_BASE_URL}/flush-to-db` : `${API_BASE_URL}/load-from-db`;
    let message = (action === 'flush') ? 'Flushing cache to DB...' : 'Rolling back cache from DB...';

    if (!confirm(`Are you sure you want to perform the action: ${action.toUpperCase()}? This will overwrite the target configuration.`)) {
        return;
    }

    const modal = document.getElementById('consistencyModal');
    if (modal) modal.style.display = 'none'; 

    const button = document.getElementById('consistency-button');
    if (button) {
        button.textContent = message;
        button.classList.remove('consistent', 'inconsistent', 'error');
        button.classList.add('loading');
        button.disabled = true;
    }

    try {
        const response = await fetch(url, { method: 'POST' });

        if (!response.ok) {
            const errorBody = await response.text();
            throw new Error(`HTTP Error ${response.status}: ${errorBody}`);
        }

        alert(`Sync successful via ${action.toUpperCase()}. Reloading data.`);

        cleanupConfigStore(); 
        
        window.loadAllData();
        checkConsistency(); 
    } catch (error) {
        alert(`Failed to sync via ${action}. Check console for details.`);
        console.error(`Sync operation (${action}) failed:`, error);
        checkConsistency(); 
    }
}


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

    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);
}


export function manualFlush() {
    resolveConsistency('flush');
}

export function manualRollback() {
    resolveConsistency('rollback');
}


// // =========================================================================
// // COMBINED LOADER & POLLING
// // =========================================================================
// export function loadAllData() {
//     window.listClusters?.(); // Use imported/window function
//     window.listListeners?.();
// }

// Attach exported cluster/modal functions to window for inline HTML calls
// These references ensure functions are callable from HTML even if imported
window.showConfigModal = showConfigModal;
window.hideModal = hideModal;
window.showClusterConfigModal = showClusterConfigModal;
window.showListenerConfigModal = showListenerConfigModal;

window.showConsistencyModal = showConsistencyModal;
window.hideConsistencyModal = hideConsistencyModal;
window.checkConsistency = checkConsistency;
window.downloadYaml = downloadYaml;
window.manualFlush = manualFlush;
window.manualRollback = manualRollback;
window.showAddFilterChainModal = showAddFilterChainModal;
window.hideAddFilterChainModal = hideAddFilterChainModal;
window.submitNewFilterChain = submitNewFilterChain;
window.cleanupConfigStore = cleanupConfigStore;

// IMPORTED CLUSTER functions must be set on window here
import { listClusters, disableCluster, enableCluster, removeCluster, showAddClusterModal, hideAddClusterModal, submitNewCluster } from './clusters.js';
import { loadAllData } from './data_loader.js';
import {showDomainConfig} from '/data_fetchers.js'
window.listClusters = listClusters;
window.disableCluster = disableCluster;
window.enableCluster = enableCluster;
window.removeCluster = removeCluster;
window.showAddClusterModal = showAddClusterModal;
window.hideAddClusterModal = hideAddClusterModal;
window.loadAllData = loadAllData;
window.submitNewCluster = submitNewCluster;
window.showDomainConfig = showDomainConfig;


window.onload = () => {
    window.loadAllData();
    setupModalTabs();
    checkConsistency(); 
    setInterval(checkConsistency, CONSISTENCY_POLL_INTERVAL);
};