// modals.js
import { setInconsistencyData, inconsistencyData } from './global.js';
import { loadAllData } from './data_loader.js'; // Will be imported later
import { configStore, API_BASE_URL } from './global.js';
// =========================================================================
// UTILITY HANDLERS (Copy & Download)
// =========================================================================
export function copyToClipboard(tabName) {
let contentToCopy = '';
// Get content safely
if (tabName === 'json') {
const jsonContentElement = document.getElementById('modal-json-content');
contentToCopy = jsonContentElement?.textContent?.trim() || '';
} else if (tabName === 'yaml') {
const yamlContentElement = document.getElementById('modal-yaml-content');
contentToCopy = yamlContentElement?.textContent?.trim() || '';
}
if (!contentToCopy) {
alert(`No ${tabName.toUpperCase()} content found to copy.`);
return;
}
// Clipboard API
navigator.clipboard.writeText(contentToCopy)
.then(() => {
const copyButton = document.getElementById('copy-config-button');
if (copyButton) {
// Instead of replacing innerHTML (which may destroy icons/styles),
// we temporarily change text via a data attribute or title
const originalLabel = copyButton.dataset.label || copyButton.textContent;
copyButton.dataset.label = originalLabel;
copyButton.classList.add('copied'); // optional styling via CSS
copyButton.textContent = '✅ Copied!';
setTimeout(() => {
copyButton.textContent = originalLabel;
copyButton.classList.remove('copied');
}, 1500);
} else {
alert('Configuration copied to clipboard!');
}
})
.catch(err => {
console.error('Failed to copy text: ', err);
alert('Failed to copy text. Please try again or check browser permissions.');
});
}
/**
* Downloads the YAML configuration content.
*/
export function downloadYaml() {
const yamlContent = document.getElementById('modal-yaml-content').textContent;
if (!yamlContent || yamlContent.trim() === '') {
alert("No YAML content available to download.");
return;
}
// Use modal title as filename fallback
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);
}
// =========================================================================
// GENERIC MODAL HANDLERS
// =========================================================================
/**
* Shows any modal element by its ID. (NEW GENERIC FUNCTION)
* @param {string} modalId - The ID of the modal element to display.
*/
export function showModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'block';
}
}
/**
* Hides any modal element by its ID. (NEW GENERIC FUNCTION)
* @param {string} modalId - The ID of the modal element to hide. Defaults to 'configModal'.
*/
export function hideModal(modalId = 'configModal') {
const modal = document.getElementById(modalId);
modal?.querySelector('.modal-content')?.querySelector(".tab-controls")?.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
if (modal) {
modal.style.display = 'none';
}
}
// =========================================================================
// CONFIGURATION DISPLAY MODAL HANDLERS (JSON/YAML tabs)
// =========================================================================
export function switchTab(modalContent, tabName) {
// Deactivate all tab buttons
const tabSelection = modalContent.querySelector('.tab-selection');
if (tabSelection) {
tabSelection.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
}
// Hide all code blocks
modalContent.querySelectorAll('.code-block').forEach(content => {
content.style.display = 'none';
});
// Activate selected tab button and 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';
// Ensure copy button copies from the correct tab
const copyButton = document.getElementById('copy-config-button');
if (copyButton) {
// Remove any existing event listener safely
const newHandler = () => copyToClipboard(tabName);
copyButton.replaceWith(copyButton.cloneNode(true)); // clone to remove old listeners
const newCopyButton = document.getElementById('copy-config-button');
newCopyButton.addEventListener('click', newHandler);
}
}
/**
* Displays the main configuration modal (with JSON/YAML tabs). (RENAMED FROM showModal)
* @param {string} title - The modal title.
* @param {object} jsonData - The configuration data object (for JSON tab).
* @param {string} yamlData - The configuration data as a YAML string.
* @param {string} [defaultTab='yaml'] - The tab to show by default.
*/
export function showConfigModal(title, jsonData, yamlData, defaultTab = 'yaml') {
document.getElementById('modal-title').textContent = title;
// 1. Check if the 'yaml' key exists in the jsonData object.
// The "yaml" attribute is only used for storing yaml context, when we display it in the frontent,
// we should exclude it from the display to confuse the user.
if (jsonData && jsonData.yaml !== undefined) {
// 2. Use the delete operator to remove the key
delete jsonData.yaml;
}
// 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) {
// This call to switchTab will also set the initial handler for the copy button
switchTab(modalContent, defaultTab);
}
// Use generic showModal
showModal('configModal');
}
/**
* Hides the main configuration modal (configModal). (Uses generic hideModal)
*/
export function hideConfigModal() {
hideModal('configModal');
}
/**
* Sets up click handlers for the tab buttons in the main modal.
*/
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');
// The switchTab function now includes the logic to update the copy button's target
switchTab(modalContent, tabName);
});
});
}
// =========================================================================
// ADD FILTER CHAIN MODAL HANDLERS
// =========================================================================
/**
* Shows the modal for adding a new filter chain to a listener.
* @param {string} listenerName - The name of the listener to modify.
*/
export function showAddFilterChainModal(listenerName) {
// 1. Set the listener name in the hidden input for form submission
document.getElementById('add-fc-listener-name').value = listenerName;
// 2. Set the title
document.getElementById('add-fc-modal-title').textContent =
`Add New Filter Chain to: ${listenerName}`;
// 3. Clear any previous YAML content
const yamlInput = document.getElementById('add-fc-yaml-input');
yamlInput.value = '';
// 4. Show the modal (using generic showModal)
showModal('addFilterChainModal');
// 5. Provide a template to guide the user (optional)
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"]
...`;
}
/**
* Closes the Add Filter Chain modal. (Uses generic hideModal)
*/
export function hideAddFilterChainModal() {
hideModal('addFilterChainModal');
}
// =========================================================================
// CONSISTENCY MODAL HANDLERS
// =========================================================================
export function showConsistencyModal() {
if (!inconsistencyData || inconsistencyData.inconsistent === false) return;
// Populate modal content
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);
// Use generic showModal
showModal('consistencyModal');
}
export function hideConsistencyModal() {
hideModal('consistencyModal');
}
// =========================================================================
// ADD CLUSTER MODAL HANDLERS (Placeholder, assuming definitions are elsewhere)
// =========================================================================
// Assuming these are defined elsewhere and attached to window,
// or are intended to be moved here:
// export function showAddClusterModal() { showModal('addClusterModal'); }
// export function hideAddClusterModal() { hideModal('addClusterModal'); }
// =========================================================================
// WINDOW EVENT LISTENERS
// =========================================================================
window.addEventListener('keydown', (event) => {
// Check for Escape key to close all modals
if (event.key === 'Escape') {
hideConfigModal();
hideAddFilterChainModal();
hideConsistencyModal();
// Assuming other hideModal functions (like hideAddClusterModal) are either imported or globally attached.
// For example:
// window.hideAddClusterModal?.();
// window.hideAddListenerModal?.();
// window.hideAddSecretModal?.();
}
});
// 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 consistencyModal = document.getElementById('consistencyModal');
const addListenerModal = document.getElementById('addListenerModal');
const addClusterModal = document.getElementById('addClusterModal');
if (event.target === modal) {
hideConfigModal();
}
if (event.target === addFCModal) {
hideAddFilterChainModal();
}
if (event.target === consistencyModal) {
hideConsistencyModal();
}
if (event.target === addListenerModal) {
// Call global function if defined externally
// window.hideAddListenerModal?.();
// Or hide using generic function if it belongs here:
hideModal('addListenerModal');
}
if (event.target === addClusterModal) {
// Call global function if defined externally
// window.hideAddClusterModal?.();
// Or hide using generic function if it belongs here:
hideModal('addClusterModal');
}
});
// =========================================================================
// CONFIG-SPECIFIC MODAL LAUNCHERS (Fetch & Display)
// =========================================================================
/**
* Fetches and displays the full configuration for a listener in the tabbed modal.
*/
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}`;
}
}
// Use the renamed function
showConfigModal(`Full Config for Listener: ${listenerName}`, config, yamlData);
}
/**
* Fetches and displays the full configuration for a secret in the tabbed modal.
*/
export async function showSecretConfigModal(secretName) {
const config = configStore.secrets[secretName];
if (!config) {
showConfigModal(`🚨 Error: Secret Not Found`, { name: secretName, error: 'Configuration data missing from memory.' }, 'Error: Secret not in memory.');
return;
}
let yamlData = configStore.secrets[secretName]?.yaml || 'Loading YAML...';
if (yamlData === 'Loading YAML...') {
try {
const response = await fetch(`${API_BASE_URL}/get-secret?name=${secretName}&format=yaml`);
if (!response.ok) {
yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`;
} else {
yamlData = await response.text();
configStore.secrets[secretName].yaml = yamlData; // Store YAML
}
} catch (error) {
console.error("Failed to fetch YAML listener config:", error);
yamlData = `Network Error fetching YAML: ${error.message}`;
}
}
// Use the renamed function
showConfigModal(`Full Config for Secret: ${secretName}`, config, yamlData);
}
/**
* Fetches and displays the full configuration for a cluster in the tabbed modal.
*/
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);
}
// =========================================================================
// NEW: EXTENSION CONFIG MODAL HANDLER
// =========================================================================
/**
* Fetches and displays the full configuration for an ExtensionConfig in the tabbed modal.
*/
export async function showExtensionConfigModal(configName) {
const config = configStore.extension_configs[configName];
if (!config) {
showConfigModal(`🚨 Error: ExtensionConfig Not Found`, { name: configName, error: 'Configuration data missing from memory.' }, 'Error: ExtensionConfig not in memory.');
return;
}
let yamlData = configStore.extension_configs[configName]?.yaml || 'Loading YAML...';
if (yamlData === 'Loading YAML...') {
try {
const response = await fetch(`${API_BASE_URL}/get-extensionconfig?name=${configName}&format=yaml`);
if (!response.ok) {
yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`;
} else {
yamlData = await response.text();
configStore.extension_configs[configName].yaml = yamlData; // Store YAML
}
} catch (error) {
console.error("Failed to fetch YAML ExtensionConfig:", error);
yamlData = `Network Error fetching YAML: ${error.message}`;
}
}
showConfigModal(`Full Config for ExtensionConfig: ${configName}`, config, yamlData);
}