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 @@
-
Add New Listener
+
Add/Update Listener
×
-
Paste the full YAML configuration for the new listener below and
- submit.
-
+
Paste the full YAML configuration for the new listener(s)
+ below and submit. You can provide a single listener object, or a
+ list of listeners under the resources key to load
+ multiple at once:
+
resources:
+ - <listener 1>
+ - <listener 2>
+
Make sure every listener includes the required type identifier:
+ "@type": type.googleapis.com/envoy.config.listener.v3.Listener,
+ which allows the parser to recognize your configuration.
+
+
+
+
+
Add/Update Cluster
+ ×
+
+
+
Paste the full YAML configuration for the new cluster(s)
+ below and submit. You can provide a single cluster object, or a
+ list of clusters under the resources key to load
+ multiple at once:
+
resources:
+ - <cluster 1>
+ - <cluster 2>
Or:
<cluster 1>
+
Make sure every cluster includes the required type identifier:
+ "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster,
+ which allows the parser to recognize your configuration.
+
+
+
+
+
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 =
- '
';
+ // 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 = `
🚨 Listener Error: ${error.message}
`;
+ 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;