// 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: {},
extension_configs: {} // NEW: Storage for ExtensionConfig data
// 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?.();
window.hideAddSecretModal?.();
window.hideAddExtensionConfigModal?.(); // NEW: Close ExtensionConfig modal
window.hideCertificateDetailsModal?.()
window.hideRotationSettingsModal?.();
}
});
window.addEventListener('click', (event) => {
// 1. Check if the clicked element has the 'modal' class (i.e., is a backdrop)
if (event.target.classList.contains('modal')) {
const modalId = event.target.id;
// 2. Map the modal ID to its corresponding close function
switch (modalId) {
case 'configModal':
// The general configuration/details modal
hideModal();
break;
case 'secretConfigModal':
// Note: The HTML provided doesn't show this ID, but it's in your original JS.
document.getElementById('secretConfigModal').style.display = 'none';
break;
case 'addFilterChainModal':
window.hideAddFilterChainModal?.();
break;
case 'addListenerModal':
window.hideAddListenerModal?.();
break;
case 'addClusterModal':
window.hideAddClusterModal?.();
break;
case 'addSecretModal':
window.hideAddSecretModal?.();
break;
case 'addExtensionConfigModal': // NEW: Close ExtensionConfig modal
window.hideAddExtensionConfigModal?.();
break;
case 'certificateDetailsModal':
window.hideCertificateDetailsModal?.();
break;
case 'rotationSettingsModal':
window.hideRotationSettingsModal?.();
break;
case 'consistencyModal':
// Note: The HTML has an inline onclick, but for consistency, we call the global function.
window.hideConsistencyModal?.();
break;
// Add any future modal IDs here
}
}
});
// 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
// // =========================================================================
// (Removed internal launcher functions to avoid redundancy)
// =========================================================================
// 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();
const upsertCheckbox = document.getElementById('add-filter-chain-upsert-flag');
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 };
if (upsertCheckbox && upsertCheckbox.checked) {
payload.upsert = true;
}
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 update 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';
}
// =========================================================================
// CONSISTENCY LOGIC
// =========================================================================
/**
* Cleans up the cached YAML data in configStore for all resources.
*/
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...';
}
}
for (const name in configStore.extension_configs) { // NEW: Cleanup ExtensionConfigs
if (configStore.extension_configs.hasOwnProperty(name)) {
configStore.extension_configs[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);
}
}
// (Removed resolveConsistency, manualFlush, manualRollback helper functions as they were commented out)
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);
}
// (Removed manualFlush, manualRollback window attachments as the functions are removed)
// Attach exported core functions to window for inline HTML calls
window.showConfigModal = showConfigModal;
window.hideModal = hideModal;
window.showConsistencyModal = showConsistencyModal;
window.hideConsistencyModal = hideConsistencyModal;
window.checkConsistency = checkConsistency;
window.downloadYaml = downloadYaml;
window.showAddFilterChainModal = showAddFilterChainModal;
window.hideAddFilterChainModal = hideAddFilterChainModal;
window.submitNewFilterChain = submitNewFilterChain;
window.cleanupConfigStore = cleanupConfigStore;
// IMPORTED MODULES and function attachments to window
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, manualRenewCertificate, hideRotationSettingsModal} from './secrets.js';
import { showListenerConfigModal ,showClusterConfigModal,showSecretConfigModal} from './modals.js';
import { listListeners, removeFilterChainByRef, disableListener, enableListener, removeListener, showAddListenerModal, hideAddListenerModal, submitNewListener } from './listeners.js';
import { listExtensionConfigs, showAddExtensionConfigModal, hideAddExtensionConfigModal, submitNewExtensionConfig, disableExtensionConfig, enableExtensionConfig, removeExtensionConfig } from './extension_configs.js'; // NEW IMPORT
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.manualRenewCertificate = manualRenewCertificate;
window.hideRotationSettingsModal = hideRotationSettingsModal;
window.listListeners = listListeners;
window.removeFilterChainByRef = removeFilterChainByRef;
window.disableListener = disableListener;
window.enableListener = enableListener;
window.removeListener = removeListener;
window.submitNewListener = submitNewListener;
window.showAddListenerModal = showAddListenerModal;
window.hideAddListenerModal = hideAddListenerModal;
// NEW EXTENSION CONFIG WINDOW ATTACHMENTS
window.listExtensionConfigs = listExtensionConfigs;
window.showAddExtensionConfigModal = showAddExtensionConfigModal;
window.hideAddExtensionConfigModal = hideAddExtensionConfigModal;
window.submitNewExtensionConfig = submitNewExtensionConfig;
window.disableExtensionConfig = disableExtensionConfig;
window.enableExtensionConfig = enableExtensionConfig;
window.removeExtensionConfig = removeExtensionConfig;
window.onload = () => {
window.loadAllData();
setupModalTabs();
checkConsistency();
setInterval(checkConsistency, CONSISTENCY_POLL_INTERVAL);
};