Newer
Older
EnvoyControlPlane / static / listeners.js
// listeners.js
import { 
    API_BASE_URL, 
    configStore, 
    showListenerConfigModal, // Re-import from global
    cleanupConfigStore,      // Re-import from global  
} from './global.js';
// We assume 'showModal' is defined elsewhere or is not needed as all modals are now handled directly
// Or, if showModal is used, it should be a general modal function from global.js or a separate modals.js file. 
// For this consolidation, we'll assume showModal is a simple function defined here or in a helper file.

// NOTE: Since global.js now exports showListenerConfigModal and showDomainConfig,
// we don't need to import them from a non-existent './modals.js' or './data_fetchers.js'.


// =========================================================================
// MODAL HELPERS (If showModal is truly a helper needed by this module, define it)
// =========================================================================
// Assuming a simplified showModal is needed for a single element ID:
function showModal(modalId) {
    const modal = document.getElementById(modalId);
    if (modal) {
        modal.style.display = 'block';
    }
}

// =========================================================================
// LISTENER UTILITIES
// =========================================================================

function getDomainRouteTable(filterChains, listenerName) {
    if (!filterChains || filterChains.length === 0) return 'N/A';
    
    // Store filter chains in memory for robust retrieval in showDomainConfig (imported from global)
    const listenerStore = configStore.listeners[listenerName] || {};
    listenerStore.filterChains = filterChains;
    
    // Store the listener configuration back into the store
    configStore.listeners[listenerName] = listenerStore;


    const domainConfigs = filterChains.map((chain, index) => {
        const domains = chain.filter_chain_match?.server_names || ["(default)"];
        const filters = chain.filters?.map(f => f.name.replace(/^envoy\.filters\./, '')) || [];
        const routeType = filters.some(f => f.includes('http_connection_manager')) ? 'HTTP' : 'TCP';
        
        const primaryDomainName = domains[0]; 

        const allDomainsTitle = domains.join(', ');
        const modalTitle = `Filter Chain for Domains: ${allDomainsTitle} (${listenerName})`;

        // Use the listener name and chain index for a unique memory reference
        const memoryKey = `${listenerName}_${index}`;
        
        // Temporarily store the domains array with the key so removeFilterChain can retrieve it
        if (!configStore.filterChainDomains) {
             configStore.filterChainDomains = {};
        }
        configStore.filterChainDomains[memoryKey] = domains;


        // Handlers are attached to the window via global.js or this file's export/window attachment
        return `
            <div class="domain-config-item">
                <div class="domain-header">
                    <div class="domain-name-link"
                        data-title="${modalTitle}" 
                        data-listener-name="${listenerName}"
                        data-chain-index="${index}" data-domain-name="${primaryDomainName}" 
                        onclick="window.showDomainConfig(this)"> 
                        ${allDomainsTitle}
                    </div>
                    <div class="filter-list">
                        ${filters.map(f => `<span class="filter-badge">${f}</span>`).join('')}
                    </div>
                </div>
                <div class="route-type-display">
                    ${routeType} Route
                    <button class="action-button remove-chain-button" 
                            title="Remove Filter Chain for ${allDomainsTitle}"
                            onclick="window.removeFilterChainByRef('${listenerName}', '${memoryKey}', event)">
                        Remove Chain
                    </button>
                </div>
            </div>
        `;
    });

    return `<div class="domain-config-list">${domainConfigs.join('')}</div>`;
}

function getListenerRowData(listener) {
    const socketAddr = listener.address?.Address?.SocketAddress;
    const address = socketAddr?.address || 'N/A';
    const port = socketAddr?.PortSpecifier?.PortValue || 'N/A';
    const addressString = `${address}:${port}`;
    const isTlsInspector = listener.listener_filters?.some(f => f.name.includes('tls_inspector'));
    const isTlsInChain = listener.filter_chains?.some(c => c.transport_socket);
    const tlsIndicator = (isTlsInspector || isTlsInChain)
        ? '<span class="tls-badge">TLS</span>'
        : '';

    return {
        name: listener.name,
        address: `${addressString} ${tlsIndicator}`,
        domains: getDomainRouteTable(listener.filter_chains, listener.name),
        status: listener.status || 'Enabled',
        rawData: listener
    };
}

// =========================================================================
// CORE API LOGIC
// =========================================================================

async function toggleListenerStatus(listenerName, action) {
    let url = '';
    if (action === 'remove') {
        url = `${API_BASE_URL}/remove-listener`;
    } else {
        url = `${API_BASE_URL}/${action}-listener`;
    }
    
    const payload = { name: listenerName };

    try {
        const response = await fetch(url, {
            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}`);
        }

        console.log(`Listener '${listenerName}' successfully ${action}d.`);
        cleanupConfigStore(); // Clean up global cache
        listListeners(); // Reload listeners
    } catch (error) {
        console.error(`Failed to ${action} listener '${listenerName}':`, error);
        alert(`Failed to ${action} listener '${listenerName}'. Check console for details.`);
    }
}

/**
 * Public export for refreshing the listener list.
 */
export async function listListeners() {
    const tableBody = document.getElementById('listener-table-body');
    if (!tableBody) {
        console.error("Could not find element with ID 'listener-table-body'.");
        return; 
    }
    
    tableBody.innerHTML =
        '<tr><td colspan="5" style="text-align: center; padding: 20px;">Loading...</td></tr>';

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

        const listenerResponse = await response.json();
        const allListeners = [
            ...(listenerResponse.enabled || []).map(l => ({ ...l, status: 'Enabled', configData: l })),
            ...(listenerResponse.disabled || []).map(l => ({ ...l, status: 'Disabled', configData: l }))
        ];

        if (!allListeners.length) {
            tableBody.innerHTML =
                '<tr><td colspan="5" style="text-align: center; color: var(--secondary-color);">No listeners found.</td></tr>';
            // Clear store if no listeners are found
            configStore.listeners = {};
            return;
        }

        // Store full configs in memory by name
        configStore.listeners = allListeners.reduce((acc, l) => {
            // Preserve existing YAML, and initialize filterChains
            const existing = acc[l.name] || {};
            acc[l.name] = { 
                ...l.configData, 
                yaml: existing.yaml,
                // Do not reset filterChains here; getDomainRouteTable will set the *new* list.
                filterChains: existing.filterChains 
            };
            return acc;
        }, configStore.listeners); 

        tableBody.innerHTML = '';
        allListeners.forEach(listener => {
            // Call getDomainRouteTable, which now stores filterChains in memory
            const rowData = getListenerRowData(listener); 
            
            const row = tableBody.insertRow();
            if (rowData.status === 'Disabled') row.classList.add('disabled-row');

            let actionButtons = '';
            
            // Add Filter Chain button (references function in global.js, attached to window)
            actionButtons += `<button class="action-button add" onclick="window.showAddFilterChainModal('${listener.name}')">Add</button>`;
            
            // Existing logic for disable/enable/remove (references functions in this module, attached to window)
            if (rowData.status === 'Enabled') {
                actionButtons += `<button class="action-button disable" onclick="window.disableListener('${listener.name}', event)">Disable</button>`;
            } else {
                // When disabled, show Enable and Remove buttons
                actionButtons += `
                    <button class="action-button enable" onclick="window.enableListener('${listener.name}', event)">Enable</button>
                    <button class="action-button remove" onclick="window.removeListener('${listener.name}', event)">Remove</button>
                `;
            }

            // Listener Name Hyperlink (Updated to use new modal function from global)
            const listenerNameCell = row.insertCell();
            listenerNameCell.innerHTML = 
                `<a href="#" onclick="event.preventDefault(); window.showListenerConfigModal('${listener.name}')"><span class="listener-name">${rowData.name}</span></a>`;
                
            row.insertCell().textContent = rowData.status;
            row.insertCell().innerHTML = rowData.address;
            row.insertCell().innerHTML = rowData.domains;
            row.insertCell().innerHTML = actionButtons;
        });
    } catch (error) {
        tableBody.innerHTML = `<tr><td colspan="5" class="error" style="text-align: center;">🚨 Listener Error: ${error.message}</td></tr>`;
        console.error("Listener Fetch/Parse Error:", error);
    }
}


/**
 * Core logic to remove a specific filter chain from a listener based on its domains.
 * @param {string} listenerName - The name of the listener.
 * @param {string[]} domains - An array of domain names that identify the filter chain.
 * @param {Event} event - The click event to stop propagation.
 */
export async function removeFilterChain(listenerName, domains, event) {
    if (event) event.stopPropagation();

    const domainList = domains.join(', ');
    if (domains.length === 1 && domains[0] === '(default)') {
         // Special handling for default chain
        if (!confirm(`⚠️ WARNING: You are about to remove the DEFAULT filter chain for listener: ${listenerName}. This will likely break the listener. Continue?`)) {
            return;
        }
    } else if (!confirm(`Are you sure you want to REMOVE the filter chain for domains: ${domainList} on listener: ${listenerName}?`)) {
        return;
    }

    const url = `${API_BASE_URL}/remove-filter-chain`;
    
    const payload = { 
        listener_name: listenerName,
        domains: domains
    };

    try {
        const response = await fetch(url, {
            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}`);
        }

        console.log(`Filter chain for domains '${domainList}' on listener '${listenerName}' successfully removed.`);
        cleanupConfigStore(); // Clean up global cache
        listListeners(); // Reload the listener list to refresh the UI
    } catch (error) {
        console.error(`Failed to remove filter chain for '${domainList}' on listener '${listenerName}':`, error);
        alert(`Failed to remove filter chain for '${domainList}' on listener '${listenerName}'. Check console for details.`);
    }
}

/**
 * Submits the new listener YAML to the /add-listener endpoint.
 */
export async function submitNewListener() {
    const yamlInput = document.getElementById('add-listener-yaml-input');
    const listenerYaml = yamlInput.value.trim();

    if (!listenerYaml) {
        alert('Please paste the Listener YAML configuration.');
        return;
    }

    try {
        // Simple YAML validation is assumed to be handled by js-yaml globally
        
        const payload = { 
            yaml: listenerYaml 
        };
        const url = `${API_BASE_URL}/add-listener`;
        
        const response = await fetch(url, {
            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}`);
        }

        console.log(`New listener successfully added.`);
        alert('Listener successfully added! The dashboard will now refresh.');
        
        // Clear input and hide modal
        yamlInput.value = '';
        hideAddListenerModal(); 
        
        cleanupConfigStore(); // Clean up global cache
        listListeners(); // Reload the listener list to refresh the UI

    } catch (error) {
        console.error(`Failed to add new listener:`, error);
        alert(`Failed to add new listener. Check console for details. Error: ${error.message}`);
    }
}


// =========================================================================
// UI EXPORTED FUNCTIONS
// =========================================================================

export function disableListener(listenerName, event) {
    event.stopPropagation();
    if (confirm(`Are you sure you want to DISABLE listener: ${listenerName}?`)) {
        toggleListenerStatus(listenerName, 'disable');
    }
}

export function enableListener(listenerName, event) {
    event.stopPropagation();
    if (confirm(`Are you sure you want to ENABLE listener: ${listenerName}?`)) {
        toggleListenerStatus(listenerName, 'enable');
    }
}

export function removeListener(listenerName, event) {
    event.stopPropagation();
    if (confirm(`⚠️ WARNING: Are you absolutely sure you want to PERMANENTLY REMOVE listener: ${listenerName}? This action cannot be undone.`)) {
        toggleListenerStatus(listenerName, 'remove');
    }
}

/**
 * UI entrypoint for removing a filter chain using a memory key.
 * @param {string} listenerName - The name of the listener.
 * @param {string} memoryKey - The key used to retrieve the domains array from configStore.
 * @param {Event} event - The click event to stop propagation.
 */
export function removeFilterChainByRef(listenerName, memoryKey, event) {
    event.stopPropagation();
    
    const domains = configStore.filterChainDomains?.[memoryKey];

    if (!domains) {
        console.error(`Error: Could not find domains array in memory for key: ${memoryKey}`);
        alert('Failed to find filter chain configuration in memory. Please refresh the page and try again.');
        return;
    }
    
    // Call the core logic with the retrieved array
    removeFilterChain(listenerName, domains, event);
}

/**
 * Shows the modal for adding a new full listener.
 */
export function showAddListenerModal() {
    // We'll use the local simple showModal if it exists, or just direct DOM manipulation
    document.getElementById('addListenerModal').style.display = 'block'; 
}

/**
 * Hides the modal for adding a new full listener.
 */
export function hideAddListenerModal() {
    const modal = document.getElementById('addListenerModal');
    if (modal) {
        modal.style.display = 'none';
        // Clear the input when closing
        document.getElementById('add-listener-yaml-input').value = ''; 
    }
}


// =========================================================================
// ATTACH TO WINDOW
// Exported functions must be attached to 'window' if called from inline HTML attributes
// =========================================================================

window.listListeners = listListeners;
window.removeFilterChainByRef = removeFilterChainByRef;
window.disableListener = disableListener;
window.enableListener = enableListener;
window.removeListener = removeListener;

// NEW FUNCTIONS ATTACHED TO WINDOW
window.submitNewListener = submitNewListener;
window.showAddListenerModal = showAddListenerModal;
window.hideAddListenerModal = hideAddListenerModal;

// Re-attach core handlers from global.js just in case, ensuring listeners.js overrides listListeners
// window.showListenerConfigModal = showListenerConfigModal; // Already attached in global.js
// window.showDomainConfig = showDomainConfig; // Already attached in global.js