// 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: {} // 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?.(); } }); // Close modal when clicking outside of the content (on the backdrop) window.addEventListener('click', (event) => { const modal = document.getElementById('configModal'); const secretModal = document.getElementById('secretConfigModal'); const addFCModal = document.getElementById('addFilterChainModal'); const addListenerModal = document.getElementById('addListenerModal'); const addClusterModal = document.getElementById('addClusterModal'); if (event.target === modal) { hideModal(); } if (event.target === secretModal) { document.getElementById('secretConfigModal').style.display = 'none'; } 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}"]`); // 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 // // ========================================================================= // /** // * 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) */ // /** // * 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...'; } } for (const name in configStore.secrets) { if (configStore.secrets.hasOwnProperty(name)) { configStore.secrets[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() { windows.resolveConsistency('flush'); } export function manualRollback() { windows.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.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' import {resolveConsistency} from './consistency.js' import { listSecrets,showAddSecretModal ,hideAddSecretModal, disableSecret, enableSecret, submitNewSecret, removeSecret, manualRenewCertificateExport} from './secrets.js'; import { showListenerConfigModal ,showClusterConfigModal,showSecretConfigModal} from './modals.js'; import { listListeners, removeFilterChainByRef, disableListener, enableListener, removeListener, showAddListenerModal, hideAddListenerModal, submitNewListener } from './listeners.js'; 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.manualRenewCertificateExport = manualRenewCertificateExport; 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; window.onload = () => { window.loadAllData(); setupModalTabs(); checkConsistency(); setInterval(checkConsistency, CONSISTENCY_POLL_INTERVAL); };