// 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: {}
// 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';
}
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 addFCModal = document.getElementById('addFilterChainModal');
const addListenerModal = document.getElementById('addListenerModal');
const addClusterModal = document.getElementById('addClusterModal');
if (event.target === modal) {
hideModal();
}
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}"]`);
const activeContent = document.getElementById(`modal-${tabName}-content`);
if (activeBtn) activeBtn.classList.add('active');
if (activeContent) activeContent.style.display = 'block';
}
export function setupModalTabs() {
const modalContent = document.getElementById('configModal')?.querySelector('.modal-content');
if (!modalContent) return;
modalContent.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', (event) => {
const tabName = event.target.getAttribute('data-tab');
switchTab(modalContent, 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)
*/
export async function showClusterConfigModal(clusterName) {
const config = configStore.clusters[clusterName];
if (!config) {
showConfigModal(`🚨 Error: Cluster Not Found`, { name: clusterName, error: 'Configuration data missing from memory.' }, 'Error: Cluster not in memory.');
return;
}
let yamlData = configStore.clusters[clusterName]?.yaml || 'Loading YAML...';
if (yamlData === 'Loading YAML...') {
try {
const response = await fetch(`${API_BASE_URL}/get-cluster?name=${clusterName}&format=yaml`);
if (!response.ok) {
yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`;
} else {
yamlData = await response.text();
configStore.clusters[clusterName].yaml = yamlData; // Store YAML
}
} catch (error) {
console.error("Failed to fetch YAML cluster config:", error);
yamlData = `Network Error fetching YAML: ${error.message}`;
}
}
showConfigModal(`Full Config for Cluster: ${clusterName}`, config, yamlData);
}
/**
* 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...';
}
}
}
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() {
resolveConsistency('flush');
}
export function manualRollback() {
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.showClusterConfigModal = showClusterConfigModal;
window.showListenerConfigModal = showListenerConfigModal;
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'
window.listClusters = listClusters;
window.disableCluster = disableCluster;
window.enableCluster = enableCluster;
window.removeCluster = removeCluster;
window.showAddClusterModal = showAddClusterModal;
window.hideAddClusterModal = hideAddClusterModal;
window.loadAllData = loadAllData;
window.submitNewCluster = submitNewCluster;
window.showDomainConfig = showDomainConfig;
window.onload = () => {
window.loadAllData();
setupModalTabs();
checkConsistency();
setInterval(checkConsistency, CONSISTENCY_POLL_INTERVAL);
};