// 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