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: {},
    secrets: {},
    extension_configs: {} // NEW: Storage for ExtensionConfig data
    // 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';
    // document.getElementById('secretConfigModal').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?.();
        window.hideAddSecretModal?.();
        window.hideAddExtensionConfigModal?.(); // NEW: Close ExtensionConfig modal
        window.hideCertificateDetailsModal?.()
        window.hideRotationSettingsModal?.();
    }
});

window.addEventListener('click', (event) => {
    // 1. Check if the clicked element has the 'modal' class (i.e., is a backdrop)
    if (event.target.classList.contains('modal')) {
        const modalId = event.target.id;
        
        // 2. Map the modal ID to its corresponding close function
        switch (modalId) {
            case 'configModal':
                // The general configuration/details modal
                hideModal();
                break;
            case 'secretConfigModal':
                // Note: The HTML provided doesn't show this ID, but it's in your original JS.
                document.getElementById('secretConfigModal').style.display = 'none';
                break;
            case 'addFilterChainModal':
                window.hideAddFilterChainModal?.();
                break;
            case 'addListenerModal':
                window.hideAddListenerModal?.();
                break;
            case 'addClusterModal':
                window.hideAddClusterModal?.();
                break;
            case 'addSecretModal':
                window.hideAddSecretModal?.();
                break;
            case 'addExtensionConfigModal': // NEW: Close ExtensionConfig modal
                window.hideAddExtensionConfigModal?.();
                break;
            case 'certificateDetailsModal':
                window.hideCertificateDetailsModal?.();
                break;
            case 'rotationSettingsModal':
                window.hideRotationSettingsModal?.();
                break;
            case 'consistencyModal':
                // Note: The HTML has an inline onclick, but for consistency, we call the global function.
                window.hideConsistencyModal?.(); 
                break;
            // Add any future modal IDs here
        }
    }
});


// 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}"]`);
    
    // Determine the content ID based on whether the modal is the regular or secret one
    const isSecret = modalContent.closest('#secretConfigModal');
    const contentIdPrefix = isSecret ? 'secret-modal' : 'modal';
    const activeContent = document.getElementById(`${contentIdPrefix}-${tabName}-content`);

    if (activeBtn) activeBtn.classList.add('active');
    if (activeContent) activeContent.style.display = 'block';
}

export function setupModalTabs() {
    const configModalContent = document.getElementById('configModal')?.querySelector('.modal-content');
    if (configModalContent) {
        configModalContent.querySelectorAll('.tab-button').forEach(button => {
            button.addEventListener('click', (event) => {
                const tabName = event.target.getAttribute('data-tab');
                switchTab(configModalContent, tabName);
            });
        });
    }

    const secretModalContent = document.getElementById('secretConfigModal')?.querySelector('.modal-content');
    if (secretModalContent) {
        secretModalContent.querySelectorAll('.tab-button').forEach(button => {
            button.addEventListener('click', (event) => {
                const tabName = event.target.getAttribute('data-tab');
                switchTab(secretModalContent, tabName);
            });
        });
    }
}

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

// (Removed internal launcher functions to avoid redundancy)

// =========================================================================
// 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();
    const upsertCheckbox = document.getElementById('add-filter-chain-upsert-flag');
    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 };

    if (upsertCheckbox && upsertCheckbox.checked) {
            payload.upsert = true;
    }

    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 update 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';
}


// =========================================================================
// CONSISTENCY LOGIC 
// =========================================================================

/**
 * Cleans up the cached YAML data in configStore for all resources.
 */
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...';
        }
    }
    for (const name in configStore.secrets) {
        if (configStore.secrets.hasOwnProperty(name)) {
            configStore.secrets[name].yaml = 'Loading YAML...';
        }
    }
    for (const name in configStore.extension_configs) { // NEW: Cleanup ExtensionConfigs
        if (configStore.extension_configs.hasOwnProperty(name)) {
            configStore.extension_configs[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);
    }
}

// (Removed resolveConsistency, manualFlush, manualRollback helper functions as they were commented out)

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

// (Removed manualFlush, manualRollback window attachments as the functions are removed)

// Attach exported core functions to window for inline HTML calls
window.showConfigModal = showConfigModal;
window.hideModal = hideModal;
window.showConsistencyModal = showConsistencyModal;
window.hideConsistencyModal = hideConsistencyModal;
window.checkConsistency = checkConsistency;
window.downloadYaml = downloadYaml;
window.showAddFilterChainModal = showAddFilterChainModal;
window.hideAddFilterChainModal = hideAddFilterChainModal;
window.submitNewFilterChain = submitNewFilterChain;
window.cleanupConfigStore = cleanupConfigStore;

// IMPORTED MODULES and function attachments to window
import { listClusters, disableCluster, enableCluster, removeCluster, showAddClusterModal, hideAddClusterModal, submitNewCluster } from './clusters.js';
import { loadAllData } from './data_loader.js';
import {showDomainConfig} from '/data_fetchers.js'
import {resolveConsistency} from './consistency.js'
import { listSecrets,showAddSecretModal ,hideAddSecretModal, disableSecret, enableSecret, submitNewSecret, removeSecret, manualRenewCertificate, hideRotationSettingsModal} from './secrets.js';
import { showListenerConfigModal ,showClusterConfigModal,showSecretConfigModal} from './modals.js';
import { listListeners, removeFilterChainByRef, disableListener, enableListener, removeListener, showAddListenerModal, hideAddListenerModal, submitNewListener } from './listeners.js';
import { listExtensionConfigs, showAddExtensionConfigModal, hideAddExtensionConfigModal, submitNewExtensionConfig, disableExtensionConfig, enableExtensionConfig, removeExtensionConfig } from './extension_configs.js'; // NEW IMPORT

window.listClusters = listClusters;
window.listSecrets = listSecrets;
window.disableCluster = disableCluster;
window.enableCluster = enableCluster;
window.removeCluster = removeCluster;
window.showAddClusterModal = showAddClusterModal;
window.showAddSecretModal = showAddSecretModal;
window.hideAddClusterModal = hideAddClusterModal;
window.loadAllData = loadAllData;
window.submitNewCluster = submitNewCluster;
window.showDomainConfig = showDomainConfig;
window.disableSecret = disableSecret;
window.enableSecret = enableSecret;
window.resolveConsistency = resolveConsistency;
window.showClusterConfigModal = showClusterConfigModal;
window.showListenerConfigModal = showListenerConfigModal;
window.hideAddSecretModal = hideAddSecretModal;
window.showSecretConfigModal = showSecretConfigModal;
window.submitNewSecret = submitNewSecret;
window.removeSecret = removeSecret;
window.manualRenewCertificate = manualRenewCertificate;
window.hideRotationSettingsModal = hideRotationSettingsModal;

window.listListeners = listListeners;
window.removeFilterChainByRef = removeFilterChainByRef;
window.disableListener = disableListener;
window.enableListener = enableListener;
window.removeListener = removeListener;
window.submitNewListener = submitNewListener;
window.showAddListenerModal = showAddListenerModal;
window.hideAddListenerModal = hideAddListenerModal;

// NEW EXTENSION CONFIG WINDOW ATTACHMENTS
window.listExtensionConfigs = listExtensionConfigs;
window.showAddExtensionConfigModal = showAddExtensionConfigModal;
window.hideAddExtensionConfigModal = hideAddExtensionConfigModal;
window.submitNewExtensionConfig = submitNewExtensionConfig;
window.disableExtensionConfig = disableExtensionConfig;
window.enableExtensionConfig = enableExtensionConfig;
window.removeExtensionConfig = removeExtensionConfig;

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