diff --git a/data/config.db b/data/config.db index 5e7a8d5..66554df 100644 --- a/data/config.db +++ b/data/config.db Binary files differ diff --git a/internal/api_handlers.go b/internal/api_handlers.go index 7332f08..5f55249 100644 --- a/internal/api_handlers.go +++ b/internal/api_handlers.go @@ -301,7 +301,7 @@ ctx := context.Background() chain, err := snapshot.LoadFilterChainFromYAML(ctx, req.YAML) if err != nil { - http.Error(w, "failed to load filter chain", http.StatusBadRequest) + http.Error(w, fmt.Sprintf("failed to load filter chain %v", err), http.StatusBadRequest) return } if err := api.Manager.AppendFilterChainToListener(ctx, req.ListenerName, chain); err != nil { diff --git a/internal/snapshot/resource_crud.go b/internal/snapshot/resource_crud.go index 35d24e6..714c9f9 100644 --- a/internal/snapshot/resource_crud.go +++ b/internal/snapshot/resource_crud.go @@ -239,7 +239,11 @@ for _, existingChain := range listener.FilterChains { existingServerNames := existingChain.GetFilterChainMatch().GetServerNames() - + if len(serverNames) == 1 && serverNames[0] == "(default)" && len(existingServerNames) == 0 { + foundMatch = true + log.Debugf("Removing default filter chain from listener '%s'", listenerName) + continue + } // Use the provided ServerNamesEqual for matching if ServerNamesEqual(existingServerNames, serverNames) { // Match found! DO NOT append this chain, effectively removing it. diff --git a/internal/snapshot/resource_io.go b/internal/snapshot/resource_io.go index 36316f7..ea381cd 100644 --- a/internal/snapshot/resource_io.go +++ b/internal/snapshot/resource_io.go @@ -9,6 +9,7 @@ clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" "github.com/envoyproxy/go-control-plane/pkg/cache/types" "github.com/envoyproxy/go-control-plane/pkg/cache/v3" diff --git a/static/clusters.js b/static/clusters.js index 89c6fc3..3efc63b 100644 --- a/static/clusters.js +++ b/static/clusters.js @@ -1,5 +1,7 @@ -import {API_BASE_URL, configStore} from './global.js' - // ========================================================================= +// clusters.js +import { API_BASE_URL, configStore, cleanupConfigStore, showClusterConfigModal } from './global.js'; + +// ========================================================================= // CLUSTER UTILITIES // ========================================================================= @@ -10,8 +12,7 @@ const lbEndpoints = endpoints[0].lb_endpoints; if (!lbEndpoints?.length) return '(No LB Endpoints)'; - // NOTE: The original logic for endpoint extraction is complex; simplify/validate path: - // assuming standard Envoy configuration structure: + // Extract address and port details const endpointObj = lbEndpoints[0].HostIdentifier?.Endpoint || lbEndpoints[0].endpoint; const address = endpointObj.address.Address.SocketAddress.address; const port = endpointObj.address.Address.SocketAddress.PortSpecifier.PortValue; @@ -24,12 +25,9 @@ } // ========================================================================= -// CLUSTER LISTING +// CLUSTER CORE LOGIC // ========================================================================= -/** - * Fetches and lists all clusters, populating the DOM table. - */ export async function listClusters() { const tableBody = document.getElementById('cluster-table-body'); if (!tableBody) { @@ -57,6 +55,7 @@ configStore.clusters = {}; return; } + cleanupConfigStore(); // Store full configs in memory by name configStore.clusters = allClusters.reduce((acc, c) => { @@ -75,12 +74,14 @@ if (cluster.status === 'Enabled') { actionButtons = ``; } else { + // When disabled, show Enable and Remove buttons actionButtons = ` `; } + // Cluster Name Hyperlink (uses showClusterConfigModal from global.js) const clusterNameCell = row.insertCell(); clusterNameCell.innerHTML = `${cluster.name}`; @@ -97,18 +98,13 @@ } } + // ========================================================================= // CLUSTER ENABLE/DISABLE/REMOVE LOGIC // ========================================================================= async function toggleClusterStatus(clusterName, action) { - let url = ''; - if (action === 'remove') { - url = `${API_BASE_URL}/remove-cluster`; - } else { - url = `${API_BASE_URL}/${action}-cluster`; - } - + let url = (action === 'remove') ? `${API_BASE_URL}/remove-cluster` : `${API_BASE_URL}/${action}-cluster`; const payload = { name: clusterName }; try { @@ -124,6 +120,7 @@ } console.log(`Cluster '${clusterName}' successfully ${action}d.`); + cleanupConfigStore(); listClusters(); } catch (error) { console.error(`Failed to ${action} cluster '${clusterName}':`, error); @@ -131,7 +128,6 @@ } } -// Exported functions must be attached to 'window' if called from inline HTML attributes export function disableCluster(clusterName, event) { event.stopPropagation(); if (confirm(`Are you sure you want to DISABLE cluster: ${clusterName}?`)) { @@ -151,4 +147,72 @@ if (confirm(`⚠️ WARNING: Are you absolutely sure you want to PERMANENTLY REMOVE cluster: ${clusterName}? This action cannot be undone.`)) { toggleClusterStatus(clusterName, 'remove'); } +} + +// ========================================================================= +// ADD CLUSTER LOGIC +// ========================================================================= + +/** + * Shows the modal for adding a new cluster. + */ +export function showAddClusterModal() { + // document.getElementById('add-cluster-modal-title').textContent = + // `Add New Cluster`; + document.getElementById('add-cluster-yaml-input').value = ''; + document.getElementById('addClusterModal').style.display = 'block'; +} + +/** + * Hides the modal for adding a new cluster. + */ +export function hideAddClusterModal() { + const modal = document.getElementById('addClusterModal'); + if (modal) { + modal.style.display = 'none'; + document.getElementById('add-cluster-yaml-input').value = ''; + } +} + + +/** + * Submits the new cluster YAML to the /add-cluster endpoint. + */ +export async function submitNewCluster() { + const yamlInput = document.getElementById('add-cluster-yaml-input'); + const clusterYaml = yamlInput.value.trim(); + + if (!clusterYaml) { + alert('Please paste the cluster YAML configuration.'); + return; + } + + try { + const payload = { yaml: clusterYaml }; + const url = `${API_BASE_URL}/add-cluster`; + + 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 cluster successfully added.`); + alert('Cluster successfully added! The dashboard will now refresh.'); + + yamlInput.value = ''; + hideAddClusterModal(); + + cleanupConfigStore(); + listClusters(); + + } catch (error) { + console.error(`Failed to add new cluster:`, error); + alert(`Failed to add new cluster. Check console for details. Error: ${error.message}`); + } } \ No newline at end of file diff --git a/static/data/logo.png b/static/data/logo.png new file mode 100644 index 0000000..c7caa1e --- /dev/null +++ b/static/data/logo.png Binary files differ diff --git a/static/global.js b/static/global.js index a7df594..7a146ce 100644 --- a/static/global.js +++ b/static/global.js @@ -3,7 +3,6 @@ export let API_BASE_URL = window.location.href; API_BASE_URL = API_BASE_URL.replace(/\/$/, ""); // Note: When reassigning, you don't repeat 'export' -// You will need to export the setter function as well since it modifies an exported 'let'. // ========================================================================= // GLOBAL IN-MEMORY STORE @@ -27,7 +26,7 @@ * @param {string} yamlData - The configuration data as a YAML string. * @param {string} defaultTab - The tab to show by default ('json' or 'yaml'). */ -function showConfigModal(title, jsonData, yamlData, defaultTab = 'yaml') { +export function showConfigModal(title, jsonData, yamlData, defaultTab = 'yaml') { document.getElementById('modal-title').textContent = title; // Populate JSON content @@ -46,29 +45,38 @@ document.getElementById('configModal').style.display = 'block'; } -function hideModal() { +export function hideModal() { document.getElementById('configModal').style.display = 'none'; } window.addEventListener('keydown', (event) => { - // Check for Escape key to close both modals + // Check for Escape key to close all modals if (event.key === 'Escape') { hideModal(); - hideAddFilterChainModal(); // NEW: Check the new modal + 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 addModal = document.getElementById('addFilterChainModal'); // NEW: Get the new modal + const addFCModal = document.getElementById('addFilterChainModal'); + const addListenerModal = document.getElementById('addListenerModal'); + const addClusterModal = document.getElementById('addClusterModal'); if (event.target === modal) { hideModal(); } - // NEW: Close the new modal if clicked outside - if (event.target === addModal) { - hideAddFilterChainModal(); + if (event.target === addFCModal) { + window.hideAddFilterChainModal?.(); + } + if (event.target === addListenerModal) { + window.hideAddListenerModal?.(); + } + if (event.target === addClusterModal) { + window.hideAddClusterModal?.(); } }); @@ -87,7 +95,7 @@ if (activeContent) activeContent.style.display = 'block'; } -function setupModalTabs() { +export function setupModalTabs() { const modalContent = document.getElementById('configModal')?.querySelector('.modal-content'); if (!modalContent) return; @@ -99,146 +107,74 @@ }); } -// ========================================================================= -// CONFIG-SPECIFIC MODAL LAUNCHERS -// ========================================================================= +// // ========================================================================= +// // CONFIG-SPECIFIC MODAL LAUNCHERS +// // ========================================================================= -/** - * Handles showing the configuration for an individual FilterChain/Domain. - * This function now loads the JSON from memory and generates the YAML from it. - */ -async function showDomainConfig(element) { - const title = element.getAttribute('data-title'); - const listenerName = element.getAttribute('data-listener-name'); - // We now use index to retrieve the JSON from memory - const chainIndex = element.getAttribute('data-chain-index'); +// /** +// * 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; - } +// if (!listenerName || chainIndex === null) { +// console.error("Missing required data attributes for domain config."); +// return; +// } - // 1. Get JSON data from memory (This is the specific filter chain object) - const listener = configStore.listeners[listenerName]; - const jsonData = listener?.filterChains?.[parseInt(chainIndex)]; +// 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; - } +// 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'; +// let yamlData = 'Generating YAML from in-memory JSON...'; +// let defaultTab = 'json'; - // 2. Generate YAML from the specific Filter Chain JSON object - try { - // Ensure the jsyaml library is available. - if (typeof require === 'undefined' && typeof jsyaml === 'undefined') { - throw new Error("YAML parser (e.g., js-yaml) is required but not found."); - } +// 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; +// const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml; - // Dump the specific Filter Chain object (jsonData) directly to YAML - yamlData = yaml.dump(jsonData, { - indent: 2, - lineWidth: -1, - flowLevel: -1 - }); - defaultTab = 'yaml'; // Switch to YAML tab if generation was successful +// yamlData = yaml.dump(jsonData, { +// indent: 2, +// lineWidth: -1, +// flowLevel: -1 +// }); +// defaultTab = 'yaml'; - } catch (error) { - // Fallback to a JSON-to-YAML approximation if jsyaml is missing or fails - console.error("Failed to generate YAML from JSON. Falling back to approximation.", error); +// } 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, ''); +// 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'; // Ensure JSON is the default if YAML generation fails - } +// 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); -} +// showConfigModal(title, jsonData, yamlData, defaultTab); +// } /** - * Extracts the YAML section for a specific domain from the filterChains array. - * @param {string} yamlData - The full YAML string containing the listener configuration. - * @param {string} domainName - The domain name to search for (e.g., 'docker.jerxie.com'). - * @returns {string | null} The YAML string for the matching filterChain, or null if not found. + * Handles showing the full configuration for a Cluster. (REMAINS HERE as a launcher) */ -function extractFilterChainByDomain(yamlData, domainName) { - // 1. Check for YAML library availability - if (typeof require === 'undefined' && typeof jsyaml === 'undefined') { - console.error("Error: YAML parser (e.g., js-yaml) is required but not found."); - return null; - } - - let fullConfig; - try { - const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml; - - // Use yaml.loadAll() instead of yaml.load() to handle multi-document streams (separated by ---). - const allDocs = yaml.loadAll(yamlData); - - // Assuming the main configuration object is the first or only document in the stream - fullConfig = allDocs.find(doc => doc && typeof doc === 'object'); - - } catch (e) { - console.error("Error parsing YAML data:", e); - return null; - } - - if (!fullConfig || !Array.isArray(fullConfig.filterChains)) { - console.warn("Input YAML does not contain a 'filterChains' array, or the main document was not found."); - return null; - } - - // 2. Find the matching filter chain object - const matchingChain = fullConfig.filterChains.find(chain => { - const serverNames = chain.filter_chain_match?.server_names; - // Check if serverNames exists and includes the domainName - return serverNames && serverNames.includes(domainName); - }); - - if (!matchingChain) { - console.log(`No filterChain found for domain: ${domainName}`); - return null; - } - - // 3. Serialize the found object back to a YAML string - try { - const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml; - - // FIX: Dump the matchingChain object directly, not as an array element. - // This will make 'filterChainMatch' the root key of the resulting YAML string. - const outputYaml = yaml.dump(matchingChain, { - // Optional: Customize indentation and flow style for cleaner output - indent: 2, - lineWidth: -1, // Do not wrap lines - flowLevel: -1 // Use block style for collections - }); - - // No need to remove the initial '- ' anymore as it won't be there. - return outputYaml.trim(); - - } catch (e) { - console.error("Error dumping YAML data:", e); - return null; - } -} - - -async function showClusterConfigModal(clusterName) { +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.'); @@ -249,7 +185,6 @@ if (yamlData === 'Loading YAML...') { try { - // CALL API FOR YAML const response = await fetch(`${API_BASE_URL}/get-cluster?name=${clusterName}&format=yaml`); if (!response.ok) { yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`; @@ -263,11 +198,13 @@ } } - // Pass JSON object from memory and authoritative YAML from API/memory showConfigModal(`Full Config for Cluster: ${clusterName}`, config, yamlData); } -async function showListenerConfigModal(listenerName) { +/** + * 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.'); @@ -278,7 +215,6 @@ if (yamlData === 'Loading YAML...') { try { - // CALL API FOR YAML const response = await fetch(`${API_BASE_URL}/get-listener?name=${listenerName}&format=yaml`); if (!response.ok) { yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`; @@ -292,38 +228,28 @@ } } - // Pass JSON object from memory and authoritative YAML from API/memory showConfigModal(`Full Config for Listener: ${listenerName}`, config, yamlData); } + // ========================================================================= -// FILTER CHAIN ADDITION LOGIC (NEW) +// FILTER CHAIN ADDITION LOGIC // ========================================================================= /** * Shows the modal for adding a new filter chain to a listener. - * @param {string} listenerName - The name of the listener to modify. */ -function showAddFilterChainModal(listenerName) { - // 1. Set the listener name in the hidden input for form submission +export function showAddFilterChainModal(listenerName) { 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 document.getElementById('addFilterChainModal').style.display = 'block'; - // 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. -# Example: filter_chain_match: server_names: ["new.example.com"] filters: @@ -348,7 +274,7 @@ /** * Handles the submission of the new filter chain YAML. */ -async function submitNewFilterChain() { +export async function submitNewFilterChain() { const listenerName = document.getElementById('add-fc-listener-name').value; const yamlData = document.getElementById('add-fc-yaml-input').value.trim(); @@ -362,10 +288,7 @@ return; } - const payload = { - listener_name: listenerName, - yaml: yamlData - }; + const payload = { listener_name: listenerName, yaml: yamlData }; try { const response = await fetch(`${API_BASE_URL}/append-filter-chain`, { @@ -381,14 +304,10 @@ alert(`Successfully appended new filter chain to '${listenerName}'.`); - // Close modal document.getElementById('addFilterChainModal').style.display = 'none'; - // ⭐ NEW: Clean up cached YAML to force a fresh fetch cleanupConfigStore(); - - // Refresh listener list - listListeners(); + 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}`); @@ -398,385 +317,45 @@ /** * Closes the Add Filter Chain modal. */ -function hideAddFilterChainModal() { +export function hideAddFilterChainModal() { document.getElementById('addFilterChainModal').style.display = 'none'; } // ========================================================================= -// CLUSTER ENABLE/DISABLE/REMOVE LOGIC +// CLUSTER LOGIC (REMOVED - ONLY REFERENCES LEFT) // ========================================================================= -async function toggleClusterStatus(clusterName, action) { - let url = ''; - // Determine the API endpoint based on the action - if (action === 'remove') { - url = `${API_BASE_URL}/remove-cluster`; - } else { - // 'enable-cluster' or 'disable-cluster' - url = `${API_BASE_URL}/${action}-cluster`; - } - - const payload = { name: clusterName }; +// listClusters() logic is now imported and exposed via window.listClusters - try { - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); +// export function disableCluster(clusterName, event) { +// event.stopPropagation(); +// window.disableCluster(clusterName, event); +// } - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`HTTP Error ${response.status}: ${errorBody}`); - } +// export function enableCluster(clusterName, event) { +// event.stopPropagation(); +// window.enableCluster(clusterName, event); +// } - console.log(`Cluster '${clusterName}' successfully ${action}d.`); - - // ⭐ NEW: Clean up cached YAML to force a fresh fetch - cleanupConfigStore(); - - listClusters(); - } catch (error) { - console.error(`Failed to ${action} cluster '${clusterName}':`, error); - alert(`Failed to ${action} cluster '${clusterName}'. Check console for details.`); - } -} - -function disableCluster(clusterName, event) { - event.stopPropagation(); - if (confirm(`Are you sure you want to DISABLE cluster: ${clusterName}?`)) { - toggleClusterStatus(clusterName, 'disable'); - } -} - -function enableCluster(clusterName, event) { - event.stopPropagation(); - if (confirm(`Are you sure you want to ENABLE cluster: ${clusterName}?`)) { - toggleClusterStatus(clusterName, 'enable'); - } -} - -// Function to handle cluster removal -function removeCluster(clusterName, event) { - event.stopPropagation(); - if (confirm(`⚠️ WARNING: Are you absolutely sure you want to PERMANENTLY REMOVE cluster: ${clusterName}? This action cannot be undone.`)) { - toggleClusterStatus(clusterName, 'remove'); - } -} +// export function removeCluster(clusterName, event) { +// event.stopPropagation(); +// window.removeCluster(clusterName, event); +// } // ========================================================================= -// LISTENER ENABLE/DISABLE/REMOVE LOGIC -// ========================================================================= -async function toggleListenerStatus(listenerName, action) { - let url = ''; - // Determine the API endpoint based on the action - if (action === 'remove') { - url = `${API_BASE_URL}/remove-listener`; - } else { - // 'enable-listener' or 'disable-listener' - 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.`); - - // ⭐ NEW: Clean up cached YAML to force a fresh fetch - cleanupConfigStore(); - - listListeners(); - } catch (error) { - console.error(`Failed to ${action} listener '${listenerName}':`, error); - alert(`Failed to ${action} listener '${listenerName}'. Check console for details.`); - } -} - -function disableListener(listenerName, event) { - event.stopPropagation(); - if (confirm(`Are you sure you want to DISABLE listener: ${listenerName}?`)) { - toggleListenerStatus(listenerName, 'disable'); - } -} - -function enableListener(listenerName, event) { - event.stopPropagation(); - if (confirm(`Are you sure you want to ENABLE listener: ${listenerName}?`)) { - toggleListenerStatus(listenerName, 'enable'); - } -} - -// Function to handle listener removal -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'); - } -} - -// ========================================================================= -// CLUSTER LOGIC -// ========================================================================= -function getClusterEndpointDetails(cluster) { - try { - const endpoints = cluster.load_assignment?.endpoints; - if (!endpoints?.length) return '(No Endpoints)'; - const lbEndpoints = endpoints[0].lb_endpoints; - if (!lbEndpoints?.length) return '(No LB Endpoints)'; - - const endpointObj = lbEndpoints[0].HostIdentifier.Endpoint; - const address = endpointObj.address.Address.SocketAddress.address; - const port = endpointObj.address.Address.SocketAddress.PortSpecifier.PortValue; - - const tls = cluster.transport_socket ? 'TLS/SSL' : ''; - return `${address}:${port} ${tls}`; - } catch { - return '(Config Error)'; - } -} - -async function listClusters() { - const tableBody = document.getElementById('cluster-table-body'); - if (!tableBody) { - console.error("Could not find element with ID 'cluster-table-body'."); - return; - } - - tableBody.innerHTML = - 'Loading...'; - - try { - const response = await fetch(`${API_BASE_URL}/list-clusters`); - if (!response.ok) throw new Error(response.statusText); - - const clusterResponse = await response.json(); - - const allClusters = [ - ...(clusterResponse.enabled || []).map(c => ({ ...c, status: 'Enabled', configData: c })), - ...(clusterResponse.disabled || []).map(c => ({ ...c, status: 'Disabled', configData: c })) - ]; - - if (!allClusters.length) { - tableBody.innerHTML = - 'No clusters found.'; - // Clear store if no clusters are found - configStore.clusters = {}; - return; - } - - // Store full configs in memory by name - configStore.clusters = allClusters.reduce((acc, c) => { - // Keep existing YAML if it exists, otherwise initialize - const existingYaml = acc[c.name]?.yaml; - acc[c.name] = { ...c.configData, yaml: existingYaml }; - return acc; - }, configStore.clusters); - - - tableBody.innerHTML = ''; - allClusters.forEach(cluster => { - const row = tableBody.insertRow(); - if (cluster.status === 'Disabled') row.classList.add('disabled-row'); - - let actionButtons = ''; - if (cluster.status === 'Enabled') { - actionButtons = ``; - } else { - // When disabled, show Enable and Remove buttons - actionButtons = ` - - - `; - } - - // Cluster Name Hyperlink (Updated to use new modal function) - const clusterNameCell = row.insertCell(); - clusterNameCell.innerHTML = - `${cluster.name}`; - - row.insertCell().textContent = cluster.status; - row.insertCell().innerHTML = getClusterEndpointDetails(cluster); - row.insertCell().textContent = - `${cluster.connect_timeout?.seconds || 0}.${(cluster.connect_timeout?.nanos / 1e6 || 0).toFixed(0).padStart(3, '0')}s`; - row.insertCell().innerHTML = actionButtons; - }); - } catch (error) { - tableBody.innerHTML = `🚨 Cluster Error: ${error.message}`; - console.error("Cluster Fetch/Parse Error:", error); - } -} - -// ========================================================================= -// LISTENER LOGIC -// ========================================================================= - -function getDomainRouteTable(filterChains, listenerName) { - if (!filterChains || filterChains.length === 0) return 'N/A'; - - // Store filter chains in memory for robust retrieval in showDomainConfig - configStore.listeners[listenerName].filterChains = filterChains; - - 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'; - - // Use the first domain name for the API call, or '(default)' - const primaryDomainName = domains[0]; - - const allDomainsTitle = domains.join(', '); - const modalTitle = `Filter Chain for Domains: ${allDomainsTitle} (${listenerName})`; - - return ` -
-
- -
- ${filters.map(f => `${f}`).join('')} -
-
-
${routeType} Route
-
- `; - }); - - return `
${domainConfigs.join('')}
`; -} - -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) - ? 'TLS' - : ''; - - return { - name: listener.name, - address: `${addressString} ${tlsIndicator}`, - domains: getDomainRouteTable(listener.filter_chains, listener.name), - status: listener.status || 'Enabled', - rawData: listener - }; -} - -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 = - 'Loading...'; - - 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 = - 'No listeners found.'; - // 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, - filterChains: existing.filterChains // Reset or keep old list temporarily - }; - 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 = ''; - - // NEW: Add Filter Chain button - actionButtons += ``; - - // Existing logic for disable/enable/remove - if (rowData.status === 'Enabled') { - actionButtons += ``; - } else { - // When disabled, show Enable and Remove buttons - actionButtons += ` - - - `; - } - - // Listener Name Hyperlink (Updated to use new modal function) - const listenerNameCell = row.insertCell(); - listenerNameCell.innerHTML = - `${rowData.name}`; - - row.insertCell().textContent = rowData.status; - row.insertCell().innerHTML = rowData.address; - row.insertCell().innerHTML = rowData.domains; - row.insertCell().innerHTML = actionButtons; - }); - } catch (error) { - tableBody.innerHTML = `🚨 Listener Error: ${error.message}`; - console.error("Listener Fetch/Parse Error:", error); - } -} - -// ========================================================================= -// CONSISTENCY LOGIC +// CONSISTENCY LOGIC (REMAINS HERE) // ========================================================================= /** * Cleans up the cached YAML data in configStore for all clusters and listeners. - * This should be called after any successful configuration mutation to force - * a fresh YAML fetch the next time a config modal is opened. */ -function cleanupConfigStore() { - // Clear YAML cache for clusters +export function cleanupConfigStore() { for (const name in configStore.clusters) { if (configStore.clusters.hasOwnProperty(name)) { configStore.clusters[name].yaml = 'Loading YAML...'; } } - // Clear YAML cache for listeners for (const name in configStore.listeners) { if (configStore.listeners.hasOwnProperty(name)) { configStore.listeners[name].yaml = 'Loading YAML...'; @@ -785,20 +364,16 @@ } -export const CONSISTENCY_POLL_INTERVAL = 5000; // This was already correct - -// Must use 'export let' to allow importing modules to see the variable. +export const CONSISTENCY_POLL_INTERVAL = 5000; export let inconsistencyData = null; -// Must export the function that modifies the exported 'inconsistencyData' variable. export function setInconsistencyData(data) { inconsistencyData = data; } -function showConsistencyModal() { +export function showConsistencyModal() { if (!inconsistencyData || inconsistencyData.inconsistent === false) return; - // Populate modal content const cacheOnly = inconsistencyData['cache-only'] || {}; const dbOnly = inconsistencyData['db-only'] || {}; @@ -815,7 +390,7 @@ document.getElementById('consistencyModal').style.display = 'block'; } -function hideConsistencyModal() { +export function hideConsistencyModal() { document.getElementById('consistencyModal').style.display = 'none'; } @@ -823,12 +398,9 @@ const button = document.getElementById('consistency-button'); if (!button) return; - // Capture current state before fetch const hasPreviousConflict = inconsistencyData !== null; const isCurrentlyError = button.classList.contains('error'); - // Temporarily update the status unless it's currently an error (best to move this after fetch, or ensure it's always reset) - // For now, keep it simple and ensure it is reset at the end. button.textContent = 'Checking...'; button.classList.add('loading'); button.classList.remove('consistent', 'inconsistent', 'error'); @@ -840,17 +412,13 @@ const data = await response.json(); const consistencyStatus = data.consistent; const isNewConflict = consistencyStatus.inconsistent === true; - - // Determine if the *inconsistent* state has changed const stateChanged = isNewConflict !== hasPreviousConflict; - // Set the final button text and remove the loading indicator button.classList.remove('loading'); if (isNewConflict) { button.textContent = '🚨 CONFLICT'; if (stateChanged || isCurrentlyError) { - // Update DOM only if state flipped OR if recovering from error button.classList.remove('consistent', 'error'); button.classList.add('inconsistent'); button.disabled = false; @@ -859,7 +427,6 @@ } else { button.textContent = '✅ Consistent'; if (stateChanged || isCurrentlyError) { - // Update DOM only if state flipped OR if recovering from error button.classList.remove('inconsistent', 'error'); button.classList.add('consistent'); button.disabled = true; @@ -880,32 +447,19 @@ /** * Core function to resolve consistency by making a POST call to a sync endpoint. - * @param {string} action - 'flush' (Cache -> DB) or 'rollback' (DB -> Cache). */ async function resolveConsistency(action) { - let url = ''; - let message = ''; - - if (action === 'flush') { - url = `${API_BASE_URL}/flush-to-db`; - message = 'Flushing cache to DB...'; - } else if (action === 'rollback') { - url = `${API_BASE_URL}/load-from-db`; - message = 'Rolling back cache from DB...'; - } else { - return; - } + 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; } - // Attempt to hide the modal if it's open (it's safe if it's closed) const modal = document.getElementById('consistencyModal'); if (modal) modal.style.display = 'none'; const button = document.getElementById('consistency-button'); - // Temporarily update the status indicator during sync if (button) { button.textContent = message; button.classList.remove('consistent', 'inconsistent', 'error'); @@ -913,7 +467,6 @@ button.disabled = true; } - try { const response = await fetch(url, { method: 'POST' }); @@ -924,27 +477,25 @@ alert(`Sync successful via ${action.toUpperCase()}. Reloading data.`); - // ⭐ NEW: Clean up cached YAML to force a fresh fetch after sync cleanupConfigStore(); - loadAllData(); - checkConsistency(); // Rerun check immediately to update status + window.loadAllData(); + checkConsistency(); } catch (error) { alert(`Failed to sync via ${action}. Check console for details.`); console.error(`Sync operation (${action}) failed:`, error); - checkConsistency(); // Restore status indicator + checkConsistency(); } } -function downloadYaml() { +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, ''); @@ -962,30 +513,59 @@ } -// ========================================================================= -// MANUAL SYNC HANDLERS -// ========================================================================= - -function manualFlush() { +export function manualFlush() { resolveConsistency('flush'); } -function manualRollback() { +export function manualRollback() { resolveConsistency('rollback'); } -// ========================================================================= -// COMBINED LOADER & POLLING -// ========================================================================= -function loadAllData() { - listClusters(); - listListeners(); -} +// // ========================================================================= +// // 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 = () => { - loadAllData(); - setupModalTabs(); // Setup tab logic on load - checkConsistency(); // Initial check - setInterval(checkConsistency, CONSISTENCY_POLL_INTERVAL); // Start polling + window.loadAllData(); + setupModalTabs(); + checkConsistency(); + setInterval(checkConsistency, CONSISTENCY_POLL_INTERVAL); }; \ No newline at end of file diff --git a/static/index.html b/static/index.html index f9dc765..19a8720 100644 --- a/static/index.html +++ b/static/index.html @@ -23,15 +23,20 @@ -
- - - - -
+
+
+ + + +
+ +
+ + +
+
-

Existing Clusters (Click a row for full JSON)

+

Existing Clusters (Click a row for full JSON/YAML details)

@@ -139,7 +144,8 @@ onclick="hideAddFilterChainModal()">× -

Paste the YAML configuration for the new filter chain below. +

Paste the YAML configuration for the new filter chain + below.

Need help composing a valid configuration? Use the external: @@ -173,14 +179,22 @@

+ diff --git a/static/listeners.js b/static/listeners.js index 551f44b..dc14444 100644 --- a/static/listeners.js +++ b/static/listeners.js @@ -1,7 +1,28 @@ // listeners.js -import { API_BASE_URL, configStore } from './global.js'; -import { showListenerConfigModal, showAddFilterChainModal, showModal } from './modals.js'; -import { showDomainConfig } from './data_fetchers.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 @@ -10,12 +31,11 @@ function getDomainRouteTable(filterChains, listenerName) { if (!filterChains || filterChains.length === 0) return 'N/A'; - // Store filter chains in memory for robust retrieval in showDomainConfig - // NOTE: This assumes configStore.listeners[listenerName] is already initialized by listListeners + // 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 (important for the new logic) + // Store the listener configuration back into the store configStore.listeners[listenerName] = listenerStore; @@ -33,14 +53,13 @@ const memoryKey = `${listenerName}_${index}`; // Temporarily store the domains array with the key so removeFilterChain can retrieve it - // NOTE: This relies on the listListeners() call immediately before rendering. if (!configStore.filterChainDomains) { configStore.filterChainDomains = {}; } configStore.filterChainDomains[memoryKey] = domains; - // Use 'window.showDomainConfig' and 'window.removeFilterChainByRef' for inline HTML handlers + // Handlers are attached to the window via global.js or this file's export/window attachment return `
@@ -91,94 +110,6 @@ } // ========================================================================= -// LISTENER LISTING -// ========================================================================= - -/** - * Fetches and lists all listeners, populating the DOM table. - */ -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 = - '
'; - - 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 = - ''; - configStore.listeners = {}; - // Clear temporary domain storage too - configStore.filterChainDomains = {}; - return; - } - - // Store full configs in memory by name - configStore.listeners = allListeners.reduce((acc, l) => { - const existing = acc[l.name] || {}; - acc[l.name] = { - ...l.configData, - yaml: existing.yaml, - filterChains: existing.filterChains - }; - return acc; - }, configStore.listeners); - - // Clear temporary domain storage before generating new data - configStore.filterChainDomains = {}; - - - tableBody.innerHTML = ''; - allListeners.forEach(listener => { - const rowData = getListenerRowData(listener); - - const row = tableBody.insertRow(); - if (rowData.status === 'Disabled') row.classList.add('disabled-row'); - - let actionButtons = ''; - - // NEW: Add Filter Chain button - actionButtons += ``; - - // Existing logic for disable/enable/remove - if (rowData.status === 'Enabled') { - actionButtons += ``; - } else { - actionButtons += ` - - - `; - } - - const listenerNameCell = row.insertCell(); - listenerNameCell.innerHTML = - `${rowData.name}`; - - row.insertCell().textContent = rowData.status; - row.insertCell().innerHTML = rowData.address; - row.insertCell().innerHTML = rowData.domains; - row.insertCell().innerHTML = actionButtons; - }); - } catch (error) { - tableBody.innerHTML = ``; - console.error("Listener Fetch/Parse Error:", error); - } -} - -// ========================================================================= // CORE API LOGIC // ========================================================================= @@ -205,7 +136,8 @@ } console.log(`Listener '${listenerName}' successfully ${action}d.`); - listListeners(); + 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.`); @@ -213,14 +145,97 @@ } /** - * Removes a specific filter chain from a listener based on its domains. + * 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 = + ''; + + 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 = + ''; + // 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 += ``; + + // Existing logic for disable/enable/remove (references functions in this module, attached to window) + if (rowData.status === 'Enabled') { + actionButtons += ``; + } else { + // When disabled, show Enable and Remove buttons + actionButtons += ` + + + `; + } + + // Listener Name Hyperlink (Updated to use new modal function from global) + const listenerNameCell = row.insertCell(); + listenerNameCell.innerHTML = + `${rowData.name}`; + + row.insertCell().textContent = rowData.status; + row.insertCell().innerHTML = rowData.address; + row.insertCell().innerHTML = rowData.domains; + row.insertCell().innerHTML = actionButtons; + }); + } catch (error) { + tableBody.innerHTML = ``; + 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) { - // Note: The event is passed but only used here for stopPropagation. - // It is not strictly needed for the API call itself. if (event) event.stopPropagation(); const domainList = domains.join(', '); @@ -253,8 +268,8 @@ } console.log(`Filter chain for domains '${domainList}' on listener '${listenerName}' successfully removed.`); - // Reload the listener list to refresh the UI - listListeners(); + 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.`); @@ -262,7 +277,7 @@ } /** - * Submits the new listener YAML to the /add-listener endpoint. (NEW FUNCTION) + * Submits the new listener YAML to the /add-listener endpoint. */ export async function submitNewListener() { const yamlInput = document.getElementById('add-listener-yaml-input'); @@ -275,7 +290,6 @@ try { // Simple YAML validation is assumed to be handled by js-yaml globally - // const parsedJson = jsyaml.load(listenerYaml); // This line is for optional client-side parsing/validation const payload = { yaml: listenerYaml @@ -300,8 +314,8 @@ yamlInput.value = ''; hideAddListenerModal(); - // Reload the listener list to refresh the UI - listListeners(); + cleanupConfigStore(); // Clean up global cache + listListeners(); // Reload the listener list to refresh the UI } catch (error) { console.error(`Failed to add new listener:`, error); @@ -357,15 +371,15 @@ } /** - * Shows the modal for adding a new full listener. (NEW FUNCTION) + * Shows the modal for adding a new full listener. */ export function showAddListenerModal() { - // You must call showModal with the correct ID: 'addListenerModal' - showModal('addListenerModal'); + // 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. (NEW FUNCTION) + * Hides the modal for adding a new full listener. */ export function hideAddListenerModal() { const modal = document.getElementById('addListenerModal'); @@ -382,15 +396,17 @@ // 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; -window.showAddFilterChainModal = showAddFilterChainModal; -window.showListenerConfigModal = showListenerConfigModal; -window.showDomainConfig = showDomainConfig; // NEW FUNCTIONS ATTACHED TO WINDOW window.submitNewListener = submitNewListener; window.showAddListenerModal = showAddListenerModal; -window.hideAddListenerModal = hideAddListenerModal; \ No newline at end of file +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 \ No newline at end of file diff --git a/static/modals.js b/static/modals.js index 4e3a2f8..a598bff 100644 --- a/static/modals.js +++ b/static/modals.js @@ -171,6 +171,24 @@ hideModal('consistencyModal'); } +// ========================================================================= +// ADD CLUSTER MODAL HANDLERS +// ========================================================================= + +/** + * Shows the modal for adding a new cluster. + */ +export function showAddClusterModal() { + showModal('addClusterModal'); +} + +/** + * Hides the Add Cluster modal. + */ +function hideAddClusterModal() { + hideModal('addClusterModal'); +} + // ========================================================================= // WINDOW EVENT LISTENERS @@ -182,6 +200,7 @@ hideConfigModal(); hideAddFilterChainModal(); hideConsistencyModal(); + hideAddClusterModal(); // Assume hideAddListenerModal is also attached to window/global scope if not in modals.js // If it is in listeners.js and attached to window: window.hideAddListenerModal(); } @@ -193,7 +212,7 @@ const addFCModal = document.getElementById('addFilterChainModal'); const consistencyModal = document.getElementById('consistencyModal'); const addListenerModal = document.getElementById('addListenerModal'); // NEW - + const addClusterModal = document.getElementById('addClusterModal'); if (event.target === modal) { hideConfigModal(); } @@ -210,6 +229,9 @@ // OR, if you decide to move it here: hideModal('addListenerModal'); } + if (event.target === addClusterModal) { + hideAddClusterModal(); + } }); // ========================================================================= diff --git a/static/style.css b/static/style.css index 447011f..da30d0c 100644 --- a/static/style.css +++ b/static/style.css @@ -166,24 +166,40 @@ opacity: 0.8; } -/* Toolbar (Modernized) */ +/* ------------------------------------------------------------- */ +/* Toolbar (Modernized) - FIX APPLIED HERE */ +/* ------------------------------------------------------------- */ + .toolbar { display: flex; - justify-content: flex-start; /* MODIFIED: Align left for better flow */ + /* This pushes the .toolbar-left-group and .toolbar-right-group to opposite sides */ + justify-content: space-between; gap: 10px; - margin-bottom: 25px; /* MODIFIED: More separation */ - margin-top: 25px; /* MODIFIED: More separation */ + margin-bottom: 25px; + margin-top: 25px; padding: 10px 0; - border-top: 1px solid var(--border-color); /* NEW: Subtle separators */ + border-top: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color); } +/* Grouping for Reload/Refresh buttons on the left */ +.toolbar-left-group { + display: flex; + gap: 10px; +} + +/* Grouping for Add New buttons on the right */ +.toolbar-right-group { + display: flex; + gap: 10px; +} + .toolbar button { - background-color: #fff; /* MODIFIED: Ghost/Outlined button style */ + background-color: #fff; /* Ghost/Outlined button style for secondary actions */ color: var(--primary-color); border: 1px solid var(--primary-color); border-radius: 5px; - padding: 8px 14px; /* MODIFIED: Slightly more padding */ + padding: 8px 14px; font-size: 0.9rem; font-weight: 500; cursor: pointer; @@ -195,22 +211,26 @@ color: white; /* Text inverts to white on hover */ } -/* Primary Action Button (Add New Listener) */ -.toolbar button:last-child { - background-color: var(--primary-color); +/* Primary Action Button Styles (Targeting the 'Add New' buttons via onclick attribute) */ +.toolbar button[onclick*="showAddClusterModal"], +.toolbar button[onclick*="showAddListenerModal"] { + background-color: var(--primary-color); /* Solid background for primary actions */ color: white; - margin-left: auto; /* Push to the right */ border-color: var(--primary-color); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + /* Ensure no unwanted margin pushing */ + margin-left: 0 !important; } -.toolbar button:last-child:hover { +.toolbar button[onclick*="showAddClusterModal"]:hover, +.toolbar button[onclick*="showAddListenerModal"]:hover { background-color: #0b5ed7; border-color: #0b5ed7; } - +/* ------------------------------------------------------------- */ /* Table (Modernized - Striped and Cleaner) */ +/* ------------------------------------------------------------- */ .config-table { width: 100%; border-collapse: separate; /* MODIFIED: Allows border-radius on cells */ @@ -534,7 +554,7 @@ /* Styles for the YAML textarea input (Unified for all YAML inputs) */ #add-fc-yaml-input, -#add-listener-yaml-input { /* Unified ID for consistency */ +#add-listener-yaml-input, #add-cluster-yaml-input { /* Unified ID for consistency */ width: 100%; font-family: monospace; font-size: 0.9rem; @@ -550,7 +570,7 @@ /* Styles for the label above the textarea (Unified for all form labels) */ label[for="add-fc-yaml-input"], -label[for="add-listener-yaml-input"] { /* Unified ID for consistency */ +label[for="add-listener-yaml-input"], label[for="add-cluster-yaml-input"]{ /* Unified ID for consistency */ display: block; font-weight: 600; margin-bottom: 5px;
Loading...
No listeners found.
🚨 Listener Error: ${error.message}
Loading...
No listeners found.
🚨 Listener Error: ${error.message}