// listeners.js import { API_BASE_URL, configStore } from './global.js'; import { showListenerConfigModal, showAddFilterChainModal, showModal } from './modals.js'; import { showDomainConfig } from './data_fetchers.js'; // ========================================================================= // LISTENER UTILITIES // ========================================================================= function getDomainRouteTable(filterChains, listenerName) { if (!filterChains || filterChains.length === 0) return 'N/A'; // Store filter chains in memory for robust retrieval in showDomainConfig // NOTE: This assumes configStore.listeners[listenerName] is already initialized by listListeners const listenerStore = configStore.listeners[listenerName] || {}; listenerStore.filterChains = filterChains; // Store the listener configuration back into the store (important for the new logic) 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 // NOTE: This relies on the listListeners() call immediately before rendering. if (!configStore.filterChainDomains) { configStore.filterChainDomains = {}; } configStore.filterChainDomains[memoryKey] = domains; // Use 'window.showDomainConfig' and 'window.removeFilterChainByRef' for inline HTML handlers 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 }; } // ========================================================================= // LISTENER LISTING // ========================================================================= /** * Fetches and lists all listeners, populating the DOM table. */ 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>'; configStore.listeners = {}; // Clear temporary domain storage too configStore.filterChainDomains = {}; return; } // Store full configs in memory by name configStore.listeners = allListeners.reduce((acc, l) => { const existing = acc[l.name] || {}; acc[l.name] = { ...l.configData, yaml: existing.yaml, filterChains: existing.filterChains }; return acc; }, configStore.listeners); // Clear temporary domain storage before generating new data configStore.filterChainDomains = {}; tableBody.innerHTML = ''; allListeners.forEach(listener => { const rowData = getListenerRowData(listener); const row = tableBody.insertRow(); if (rowData.status === 'Disabled') row.classList.add('disabled-row'); let actionButtons = ''; // NEW: Add Filter Chain button actionButtons += `<button class="action-button add" onclick="window.showAddFilterChainModal('${listener.name}')">Add</button>`; // Existing logic for disable/enable/remove if (rowData.status === 'Enabled') { actionButtons += `<button class="action-button disable" onclick="window.disableListener('${listener.name}', event)">Disable</button>`; } else { 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> `; } 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 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.`); listListeners(); } catch (error) { console.error(`Failed to ${action} listener '${listenerName}':`, error); alert(`Failed to ${action} listener '${listenerName}'. Check console for details.`); } } /** * Removes 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) { // Note: The event is passed but only used here for stopPropagation. // It is not strictly needed for the API call itself. 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.`); // Reload the listener list to refresh the UI listListeners(); } 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. (NEW FUNCTION) */ 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 parsedJson = jsyaml.load(listenerYaml); // This line is for optional client-side parsing/validation 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(); // Reload the listener list to refresh the UI listListeners(); } 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. (NEW FUNCTION) */ export function showAddListenerModal() { // You must call showModal with the correct ID: 'addListenerModal' showModal('addListenerModal'); } /** * Hides the modal for adding a new full listener. (NEW FUNCTION) */ 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.removeFilterChainByRef = removeFilterChainByRef; window.disableListener = disableListener; window.enableListener = enableListener; window.removeListener = removeListener; window.showAddFilterChainModal = showAddFilterChainModal; window.showListenerConfigModal = showListenerConfigModal; window.showDomainConfig = showDomainConfig; // NEW FUNCTIONS ATTACHED TO WINDOW window.submitNewListener = submitNewListener; window.showAddListenerModal = showAddListenerModal; window.hideAddListenerModal = hideAddListenerModal;