diff --git a/data/config/cds.yaml b/data/config/cds.yaml index a18b8bd..f05264f 100755 --- a/data/config/cds.yaml +++ b/data/config/cds.yaml @@ -2,7 +2,7 @@ - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: _acme_renewer connect_timeout: 0.2s - type: STATIC_DNS + type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: acme_renewer diff --git a/static/clusters.js b/static/clusters.js deleted file mode 100755 index 071a556..0000000 --- a/static/clusters.js +++ /dev/null @@ -1,242 +0,0 @@ -// clusters.js -import { API_BASE_URL, configStore, cleanupConfigStore } from './global.js'; - -// ========================================================================= -// CLUSTER UTILITIES (Unchanged) -// ========================================================================= - -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)'; - - // 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; - - const tls = cluster.transport_socket ? 'TLS/SSL' : ''; - return `${address}:${port} ${tls}`; - } catch { - return '(Config Error)'; - } -} - -// ========================================================================= -// CLUSTER CORE LOGIC (Unchanged) -// ========================================================================= - -export 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.'; - configStore.clusters = {}; - return; - } - cleanupConfigStore(); - - // Store full configs in memory by name - configStore.clusters = allClusters.reduce((acc, c) => { - 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 (uses showClusterConfigModal from global.js) - 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); - } -} - - -// ========================================================================= -// CLUSTER ENABLE/DISABLE/REMOVE LOGIC (Unchanged) -// ========================================================================= - -async function toggleClusterStatus(clusterName, action) { - let url = (action === 'remove') ? `${API_BASE_URL}/remove-cluster` : `${API_BASE_URL}/${action}-cluster`; - const payload = { name: clusterName }; - - 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(`Cluster '${clusterName}' successfully ${action}d.`); - cleanupConfigStore(); - listClusters(); - } catch (error) { - console.error(`Failed to ${action} cluster '${clusterName}':`, error); - alert(`Failed to ${action} cluster '${clusterName}'. Check console for details.`); - } -} - -export function disableCluster(clusterName, event) { - event.stopPropagation(); - if (confirm(`Are you sure you want to DISABLE cluster: ${clusterName}?`)) { - toggleClusterStatus(clusterName, 'disable'); - } -} - -export function enableCluster(clusterName, event) { - event.stopPropagation(); - if (confirm(`Are you sure you want to ENABLE cluster: ${clusterName}?`)) { - toggleClusterStatus(clusterName, 'enable'); - } -} - -export 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'); - } -} - -// ========================================================================= -// 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 = ''; - // Clear checkbox on show - const upsertCheckbox = document.getElementById('add-cluster-upsert-flag'); - if (upsertCheckbox) { - upsertCheckbox.checked = false; - } - 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 = ''; - // Clear checkbox on hide - const upsertCheckbox = document.getElementById('add-cluster-upsert-flag'); - if (upsertCheckbox) { - upsertCheckbox.checked = false; - } - } -} - - -/** - * Submits the new cluster YAML to the /add-cluster endpoint. - * - * MODIFIED: Now checks for an 'allow-upsert' checkbox and adds 'upsert: true' to the payload. - */ -export async function submitNewCluster() { - const yamlInput = document.getElementById('add-cluster-yaml-input'); - // Get the checkbox element and its state - const upsertCheckbox = document.getElementById('add-cluster-upsert-flag'); - const clusterYaml = yamlInput.value.trim(); - - if (!clusterYaml) { - alert('Please paste the cluster YAML configuration.'); - return; - } - - try { - const payload = { yaml: clusterYaml }; - - // Add upsert flag to payload if checkbox is checked - if (upsertCheckbox && upsertCheckbox.checked) { - payload.upsert = true; - } - - 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 = ''; - // Uncheck the box upon success/closing - if (upsertCheckbox) { - upsertCheckbox.checked = false; - } - 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/consistency.js b/static/consistency.js deleted file mode 100755 index d65aa5b..0000000 --- a/static/consistency.js +++ /dev/null @@ -1,66 +0,0 @@ -// consistency.js -import { API_BASE_URL, CONSISTENCY_POLL_INTERVAL, setInconsistencyData, inconsistencyData } from './global.js'; -import { hideConsistencyModal, showConsistencyModal } from './modals.js'; -import { loadAllData } from './data_loader.js'; // Will be imported later -import { checkConsistency } from './global.js'; - -/** - * Core function to resolve consistency by making a POST call to a sync endpoint. - * @param {string} action - 'flush' (Cache -> DB) or 'rollback' (DB -> Cache). - */ -export 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; - } - - if (!confirm(`Are you sure you want to perform the action: ${action.toUpperCase()}? This will overwrite the target configuration.`)) { - return; - } - - const modal = document.getElementById('consistencyModal'); - if (modal) modal.style.display = 'none'; - - const button = document.getElementById('consistency-button'); - if (button) { - button.textContent = message; - button.classList.remove('consistent', 'inconsistent', 'error'); - button.classList.add('loading'); - button.disabled = true; - } - - try { - const response = await fetch(url, { method: 'POST' }); - - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`HTTP Error ${response.status}: ${errorBody}`); - } - - alert(`Sync successful via ${action.toUpperCase()}. Reloading data.`); - loadAllData(); - checkConsistency(); - } catch (error) { - alert(`Failed to sync via ${action}. Check console for details.`); - console.error(`Sync operation (${action}) failed:`, error); - checkConsistency(); - } -} - - -// Exported functions must be attached to 'window' if called from inline HTML attributes -export function manualFlush() { - resolveConsistency('flush'); -} - -export function manualRollback() { - resolveConsistency('rollback'); -} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100755 index 0000000..b840566 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,1075 @@ +:root { + --primary-color: #0d6efd; + --secondary-color: #6c757d; + --info-color: #0dcaf0; + --success-color: #198754; /* New: for consistent status */ + --danger-color: #dc3545; /* New: for conflict status */ + --warning-color: #ffc107; /* New: for error status */ + --border-color: #e9ecef; /* MODIFIED: Lighter border for a softer look */ + --bg-color-light: #f8f9fa; + --text-color: #212529; + --text-color-light: #495057; /* NEW: for secondary text/headers */ + + /* Light Theme Code Colors */ + --code-bg: #f8f8f8; /* Very light gray, like a parchment or fresh terminal */ + --code-text: #333333; /* Darker text for readability */ + --code-border: #e0e0e0; +} + +/* Layout */ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + margin: 0; + padding: 20px; + background-color: var(--bg-color-light); + color: var(--text-color); +} + +.container { + max-width: 1200px; + margin: 0 auto; + background: #fff; + border: 1px solid var(--border-color); + padding: 30px; + border-radius: 12px; /* MODIFIED: Larger radius for a modern feel */ + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.05); /* MODIFIED: Softer, deeper shadow */ + position: relative; +} + +/* NEW: Consistent Heading Styles */ +h1 { + font-size: 2rem; + font-weight: 700; + margin-top: 0; + margin-bottom: 5px; +} + +h2 { + font-size: 1.5rem; + font-weight: 600; + margin-top: 30px; + margin-bottom: 10px; + color: var(--text-color-light); /* Subdued color for section headers */ +} + +/* ------------------------------------------------------------- */ +/* Action Buttons (General Styles) */ +/* ------------------------------------------------------------- */ +.action-button { + font-size: 0.8rem; /* MODIFIED: Slightly smaller in the table for a cleaner fit */ + padding: 4px 8px; /* MODIFIED: Tighter padding */ + border-radius: 4px; + cursor: pointer; + border: 1px solid transparent; + transition: all 0.2s ease; + margin: 0; + margin-right: 5px; + min-width: 65px; /* NEW: Ensure uniform width in table */ + text-align: center; +} + +.action-button.disable { + background-color: #fff; /* MODIFIED: Ghost/Outlined style for better contrast */ + color: var(--secondary-color); + border-color: #dcdcdc; /* MODIFIED: Light gray border */ + font-weight: 500; +} + +.action-button.disable:hover { + background-color: var(--secondary-color); /* Solid on hover */ + color: white; +} + +.action-button.enable { + background-color: var(--success-color); + color: white; + font-weight: 500; +} + +.action-button.enable:hover { + background-color: #157347; +} + +.action-button.remove { + background-color: var(--danger-color); + color: white; +} + +.action-button.remove:hover { + background-color: #bb2d3b; +} + +/* Add Chain Button */ +.action-button.add { + background-color: var(--success-color); /* MODIFIED: Using variable for consistency */ + color: white; +} + +.action-button.add:hover { + background-color: #157347; +} + +/* Cancel button for forms (like in the Add Chain modal) */ +.action-button.cancel { + background-color: var(--secondary-color); + color: white; +} +.action-button.cancel:hover { + background-color: #5c636a; +} + +/* ------------------------------------------------------------- */ +/* Consistency Status Indicator (Modernized) */ +/* ------------------------------------------------------------- */ +#consistency-status-container { + position: absolute; + top: 30px; + right: 30px; + z-index: 5; +} + +#consistency-button { + font-size: 0.85rem; /* MODIFIED: Smaller font */ + padding: 5px 12px; /* MODIFIED: Tighter padding */ + border-radius: 20px; /* MODIFIED: Pill-shaped */ + font-weight: 700; + cursor: default; + pointer-events: none; + transition: background-color 0.2s ease, transform 0.1s ease; + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* NEW: Subtle shadow */ +} + +#consistency-button.consistent { + background-color: var(--success-color); + color: white; +} + +#consistency-button.inconsistent { + background-color: var(--danger-color); + color: white; + cursor: pointer; + pointer-events: auto; +} + +#consistency-button.inconsistent:hover { + background-color: #bb2d3b; + transform: scale(1.02); +} + +#consistency-button.error { + background-color: var(--warning-color); + color: var(--text-color); +} + +#consistency-button:disabled { + opacity: 0.8; +} + +/* ------------------------------------------------------------- */ +/* Toolbar (Modernized) - FIX APPLIED HERE */ +/* ------------------------------------------------------------- */ + +.toolbar { + display: flex; + /* This pushes the .toolbar-left-group and .toolbar-right-group to opposite sides */ + justify-content: space-between; + gap: 10px; + margin-bottom: 25px; + margin-top: 25px; + padding: 10px 0; + 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, .toolbar a { + 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; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; +} + +.toolbar button:hover, .toolbar a:hover { + background-color: var(--primary-color); + color: white; /* Text inverts to white on hover */ +} + +/* Primary Action Button Styles (Targeting the 'Add New' buttons via onclick attribute) */ +.toolbar button[onclick*="showAddClusterModal"], +.toolbar button[onclick*="showAddListenerModal"], +.toolbar button[onclick*="showAddSecretModal"], +.toolbar button[onclick*="showAddExtensionConfigModal"] { + background-color: var(--primary-color); /* Solid background for primary actions */ + color: white; + 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[onclick*="showAddClusterModal"]:hover, +.toolbar button[onclick*="showAddListenerModal"]:hover , +.toolbar button[onclick*="showAddSecretModal"]:hover , +.toolbar button[onclick*="showAddExtensionConfigModal"]: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 */ + border-spacing: 0; /* NEW: Removes spacing between borders */ + margin-top: 15px; + border: 1px solid var(--border-color); /* NEW: Single border around the table */ + border-radius: 6px; /* NEW: Rounded corners on table */ + overflow: hidden; +} + +.config-table th, +.config-table td { + padding: 12px 15px; /* MODIFIED: Increased vertical padding */ + text-align: left; + border-bottom: none; /* MODIFIED: Removed internal horizontal lines */ + vertical-align: middle; /* MODIFIED: Center align content vertically */ +} + +.config-table th { + background-color: var(--bg-color-light); /* MODIFIED: Lighter header background */ + font-weight: 600; + color: var(--text-color-light); /* MODIFIED: Subdued header text */ + position: sticky; + top: 0; + z-index: 2; + border-bottom: 1px solid var(--border-color); /* Re-add separator */ +} + +/* NEW: Zebra Striping */ +.config-table tbody tr:nth-child(even) { + background-color: #fcfcfd; +} +.config-table tbody tr:nth-child(odd) { + background-color: #fff; +} + +/* Prevent whole row hover highlight (still needed) */ +.config-table tr:hover { + background-color: transparent; +} + +/* Row-specific hover highlight (Softer blue on hover) */ +.config-table tr:hover:not(.disabled-row) { + background-color: #eef5ff; /* MODIFIED: Softer hover color */ +} + +/* Status Column Text Styling for readability */ +.config-table td:nth-child(2) { + font-family: monospace; + font-size: 0.875rem; + color: var(--text-color-light); +} + +.listener-name, +.domain-name-link { + font-weight: 600; + color: var(--primary-color); + cursor: pointer; + text-decoration: none; /* MODIFIED: Remove default underline */ + transition: color 0.15s ease; +} + +/* Highlight only when hovering over the link itself */ +.listener-name:hover, +.domain-name-link:hover { + text-decoration: underline; /* MODIFIED: Add underline only on hover */ + color: #0b5ed7; +} + +/* Keep non-clickable text normal */ +.config-table td { + cursor: default; +} + +.disabled-row { + opacity: 0.5; /* MODIFIED: Slightly more muted */ + background-color: #fbfbfb !important; +} + +.disabled-row .listener-name { + text-decoration: line-through; +} + +/* Badges */ +.tls-badge { + display: inline-block; + background-color: var(--info-color); + color: white; + padding: 4px 8px; + border-radius: 12px; + font-size: 0.75rem; + margin-left: 6px; +} + +.filter-badge { + display: inline-block; + padding: 3px 6px; + border-radius: 4px; + font-size: 0.7rem; + margin-left: 6px; +} + +/* Domain Config */ +.domain-config-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 6px; +} + +.domain-config-item { + background-color: #fcfcfc; + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 6px 10px; + transition: background-color 0.2s ease; +} + +.domain-config-item:hover { + background-color: #eef5ff; +} + +.domain-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.domain-name-link { + color: var(--primary-color); + font-weight: 500; + cursor: pointer; +} + +.domain-name-link:hover { + text-decoration: underline; +} + +/* IMPROVED: Layout for Route Type and Remove Button */ +.route-type-display { + font-size: 0.75rem; + color: var(--secondary-color); + margin-top: 3px; + + /* NEW: Use flex to align type and button on the same line */ + display: flex; + justify-content: space-between; /* Push button to the right */ + align-items: center; +} + +/* NEW: Style for the "Remove Chain" button inside the domain item */ +.action-button.remove-chain-button { + /* Override standard action button size */ + font-size: 0.7rem; + padding: 2px 6px; + border-radius: 3px; + margin: 0; + + /* Make it less aggressive than the full listener remove button */ + background-color: #ff4d4d; /* A lighter, softer red */ + color: white; + border: 1px solid #ff4d4d; + transition: all 0.2s ease; +} + +.action-button.remove-chain-button:hover { + background-color: var(--danger-color); /* Full red on hover */ + border-color: var(--danger-color); +} + +/* IMPROVED: Action column styling to stack buttons neatly (for the last column) */ +.config-table td:last-child { + display: flex; + flex-direction: column; /* Stack buttons vertically */ + gap: 4px; /* Small space between stacked buttons */ + /* Ensure padding is kept */ + padding: 10px 12px; +} + +/* Target buttons in the main Action column to ensure consistent width */ +.config-table td:last-child .action-button { + margin-right: 0; /* Remove horizontal margin */ + width: 100%; /* Make them full width for that column */ + box-sizing: border-box; + text-align: center; +} + + +/* ============================================================= */ +/* MODAL FIXES AND TAB STYLING (Consistent) */ +/* ============================================================= */ + +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + padding-top: 50px; + background-color: rgba(0, 0, 0, 0.5); + overflow: auto; +} + +.modal-content { + background-color: #fff; + margin: 0 auto; + padding: 25px; + border-radius: 8px; + max-width: 900px; + width: 80%; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3); + position: relative; + max-height: 90vh; + overflow-y: auto; +} + +/* NEW: Consistent Modal Header */ +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 10px; +} + +/* Close Button Styling and Positioning Fix */ +.close-btn, /* The one used in the secondary modals */ +.modal-content > .close { /* The one used in the main configModal (Unified) */ + color: #aaa; + font-size: 28px; + font-weight: bold; + line-height: 1; + cursor: pointer; + position: absolute; + top: 10px; + right: 20px; + z-index: 10; +} + +.close-btn:hover, +.close-btn:focus, +.modal-content > .close:hover, +.modal-content > .close:focus { + color: #000; + text-decoration: none; +} + +.modal-content h2, +.modal-content h3 { /* Apply to H2 and H3 for consistency */ + padding-right: 40px; + margin-top: 0; /* Ensures consistent top margin on modal headers */ +} + +/* --- TAB CONTROL LAYOUT --- */ +.tab-controls { + display: flex; /* Aligns tab-selection and action-group horizontally */ + align-items: flex-end; /* Aligns the tab buttons and action group to the bottom line */ + margin: 0 0 0; + /* REMOVE: border-bottom: 2px solid var(--border-color); */ +} + + +/* NEW CSS: Create a dedicated container for the gray underline */ +.tab-selection { + display: flex; + /* ADD the border-bottom here. It will only span the width of the tab buttons. */ + border-bottom: 2px solid var(--border-color); +} + +/* Wrapper for Copy/Download buttons (right side) */ +.modal-action-group { + display: flex; + gap: 10px; /* Space between Copy and Download buttons */ + align-items: center; + margin-left: auto; /* Pushes the group to the right */ + padding-bottom: 5px; /* Visual adjustment above the border */ +} + +/* --- TAB BUTTON STYLING (JSON/YAML) --- */ +.tab-button { + background: transparent; + border: none; + padding: 10px 15px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + color: var(--secondary-color); + transition: all 0.2s ease; +} + +.tab-button.active { + color: var(--primary-color); + /* The active blue underline is still correct here */ + border-bottom-color: var(--primary-color); +} + +/* Style for the Copy Button to look like an action button */ +#copy-config-button { + /* Set to look like a secondary action button */ + background-color: #fff; + color: var(--primary-color); + border: 1px solid var(--primary-color); + border-radius: 4px; + padding: 8px 12px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + + /* Override any tab-button specific styles that might clash */ + margin-bottom: 0 !important; + line-height: 1.2; +} + +#copy-config-button:hover { + background-color: var(--primary-color); + color: white; +} + +/* Download Button Styling (Standardized to match the outlined look of Copy to Clip) */ +.download-button { + /* Set to match the Copy button: outlined style */ + background-color: #fff; /* White background */ + color: var(--primary-color); /* Primary text color */ + border: 1px solid var(--primary-color); /* Primary border color */ + border-radius: 4px; + padding: 8px 15px; + font-size: 0.9rem; + font-weight: 500; +} + +.download-button:hover { + /* Solid primary color on hover for consistency */ + background-color: var(--primary-color); + border-color: #0b5ed7; + color: white; /* White text on hover */ +} + +/* Consistent Icon/Text Spacing for both action buttons */ +#copy-config-button, +.download-button { + display: flex; + align-items: center; +} + +/* Adjust spacing for the icons in both buttons */ +#copy-config-button .action-button-icon, /* Use a span class if you wrap the icon */ +.download-button .action-button-icon { /* Use a span class if you wrap the icon */ + margin-right: 5px; +} + +/* Since you are using unicode, this targets the first child (the emoji) */ +#copy-config-button:not([style*='Copied']) > *:first-child, +.download-button > *:first-child { + margin-right: 5px; /* Small space between icon and text */ +} + +/* Code Block Styling */ +.code-block { + background: var(--code-bg); + color: var(--code-text); + font-family: monospace; + font-size: 0.9rem; + padding: 15px; + border-radius: 4px; + border: 1px solid var(--code-border); + max-height: 500px; + overflow: auto; + white-space: pre; + word-wrap: normal; +} + +/* ------------------------------------------------------------- */ +/* Add Filter Chain & Add Listener Modal Styles (Unified) */ +/* ------------------------------------------------------------- */ + +/* Styles for the larger content area within the modal */ +.modal-content.large-modal { + max-width: 900px; + width: 90%; +} + +/* Styles for the YAML textarea input (Unified for all YAML inputs) */ +#add-fc-yaml-input, +#add-listener-yaml-input, #add-cluster-yaml-input , #add-secret-yaml-input, #add-extension-config-yaml-input { /* Unified ID for consistency */ + width: 100%; + font-family: monospace; + font-size: 0.9rem; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--code-bg); + color: var(--code-text); + resize: vertical; + box-sizing: border-box; + min-height: 250px; /* Ensures Add Listener textarea has a good starting height */ +} + +/* Styles for the label above the textarea (Unified for all form labels) */ +label[for="add-fc-yaml-input"], +label[for="add-listener-yaml-input"], label[for="add-cluster-yaml-input"], label[for="add-secret-yaml-input"]{ /* Unified ID for consistency */ + display: block; + font-weight: 600; + margin-bottom: 5px; + margin-top: 15px; +} + +.modal-note { + font-size: 0.85rem; + color: var(--secondary-color); + margin-top: 20px; + padding-top: 10px; + border-top: 1px solid var(--border-color); +} + + +/* ------------------------------------------------------------- */ +/* Consistency Modal Specific Styles (Enhanced) */ +/* ------------------------------------------------------------- */ + +/* Modal Action buttons container (Unified Footer) */ +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 15px; + padding-top: 15px; + border-top: 1px solid var(--border-color); + margin-top: 20px; +} + +.modal-actions button { + font-size: 1rem; + padding: 10px 20px; + line-height: 1.2; + text-align: center; + min-width: 150px; /* Consistent minimum width for action buttons */ +} + +.modal-actions small { + font-weight: 400; + display: block; + font-size: 0.75rem; + opacity: 0.9; +} + +#inconsistency-details-content pre { + /* Ensure consistency modal PRE elements also use the light code-block styles for scrolling */ + background: var(--code-bg); + color: var(--code-text); + padding: 10px; + border-radius: 4px; + border: 1px solid var(--code-border); + font-size: 0.9rem; + max-height: 300px; + overflow: auto; + white-space: pre; + word-wrap: normal; +} + +/* Consistency Conflict List styling (Enhanced) */ +.conflict-list { + list-style: none; + padding: 0; + margin: 10px 0 20px 0; + border: 1px solid var(--code-border); + border-radius: 4px; + background-color: var(--code-bg); + max-height: 200px; + overflow-y: auto; +} + +.conflict-list li { + padding: 8px 10px; + border-bottom: 1px solid var(--code-border); + font-size: 0.95rem; + display: flex; + gap: 10px; + align-items: center; +} + +.conflict-list li:last-child { + border-bottom: none; +} + +.conflict-list li:nth-child(even) { + background-color: transparent; +} + +.conflict-list .no-conflicts { + color: var(--success-color); + font-weight: 500; + background-color: #e6f7ef; + border-radius: 4px; + padding: 10px; + margin: 5px; + border: 1px solid rgba(25, 135, 84, 0.2); + text-align: center; +} + +.conflict-type { + font-weight: 600; + color: var(--primary-color); + background-color: #e9f2ff; + padding: 2px 6px; + border-radius: 4px; +} + +.conflict-name { + font-family: monospace; + font-weight: 500; + color: var(--text-color); + flex-grow: 1; +} +/* ------------------------------------------------------------- */ + + +/* Responsive */ +@media (max-width: 768px) { + .config-table th, .config-table td { + font-size: 0.85rem; + padding: 8px; + } + .container { + padding: 15px; + } + .modal-content { + width: 95%; + margin: 10px auto; + } + .modal { + padding-top: 20px; + } + #consistency-status-container { + top: 15px; + right: 15px; + } +} + +/* Style for the validity status labels */ +.status-label { + padding: 2px 8px; + border-radius: 4px; + font-size: 0.85em; + font-weight: bold; + color: white; /* Default text color */ + margin-left: 5px; + text-transform: uppercase; +} + +.status-label.invalid { + background-color: var(--error-color, #ff4d4f); /* Red color for Invalid */ +} + +.status-label.valid { + background-color: var(--success-color, #52c41a); /* Green color for Valid */ +} + +/* Custom styles for the Certificate Issuer link/button (Issue New Certificate) */ + +/* Container to ensure the heading and the button are on the same line and spaced correctly */ +.secrets-header-container { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 30px; /* Use the same top margin as H2 for proper spacing */ + margin-bottom: 10px; /* Use the same bottom margin as H2 for proper spacing */ +} + +.cert-issuer-button { + /* Style to match existing header buttons (e.g., Add/Update Cluster) */ + display: inline-flex; + align-items: center; + + /* Using toolbar button styling for consistency */ + background-color: var(--primary-color); + color: white; + border: 1px solid var(--primary-color); + border-radius: 5px; + padding: 8px 14px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + text-decoration: none; /* Remove underline from the anchor tag */ + transition: all 0.2s ease; + + /* Add a subtle shadow consistent with primary toolbar buttons */ + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +.cert-issuer-button:hover { + background-color: #0b5ed7; /* Darker primary color on hover (same as toolbar) */ + border-color: #0b5ed7; + color: white; +} + +/* Style for the text label within the button */ +.cert-issuer-button-text { + margin-right: 8px; /* Space between text and icon */ + font-size: 0.9rem; /* Match button text size */ +} + +/* Style for the plus icon within the button */ +.cert-issuer-button-icon { + font-size: 1.1rem; /* Slightly larger plus sign for emphasis */ + line-height: 1; +} + +/* ============================================================= */ +/* Rotation Settings Modal Specific Styles */ +/* ============================================================= */ + +/* General form group layout for modals */ +.modal-content .form-group { + margin-bottom: 15px; +} + +/* Style for input fields inside modals (Unified for consistency) */ +.modal-content input[type="text"], +.modal-content input[type="number"] { + width: 100%; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 4px; + box-sizing: border-box; + font-size: 1rem; + transition: border-color 0.2s ease; +} + +.modal-content input[type="text"]:focus, +.modal-content input[type="number"]:focus { + border-color: var(--primary-color); + outline: none; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +/* Style for labels (Unified for consistency) */ +.modal-content label { + display: block; + font-weight: 600; + margin-bottom: 5px; + color: var(--text-color); +} + +/* Style for the checkbox/toggle group (Unified) */ +.modal-content .checkbox-group { + display: flex; + align-items: center; + padding-left: 0; +} + +.modal-content .checkbox-group input[type="checkbox"] { + /* Style for the visual toggle or checkbox itself */ + margin-right: 10px; + width: 18px; + height: 18px; + cursor: pointer; +} + +.modal-content .checkbox-group label { + /* Adjust label positioning for checkbox */ + font-weight: 500; + margin-bottom: 0; + cursor: pointer; +} + +/* Hint text below inputs */ +.modal-content .input-hint { + font-size: 0.8rem; + color: var(--secondary-color); + margin-top: 5px; + margin-bottom: 0; + padding-left: 2px; +} + +/* Align the submit button in the footer using the existing .modal-actions */ +#rotationSettingsModal .modal-actions #rotation-submit-btn { + /* Use the standard 'add' style */ + background-color: var(--success-color); + color: white; + font-weight: 500; +} + +#rotationSettingsModal .modal-actions #rotation-submit-btn:hover { + background-color: #157347; +} + +#rotationSettingsModal .modal-actions button.disable { + /* Use the standard 'cancel' style */ + background-color: var(--secondary-color); + color: white; + border-color: var(--secondary-color); +} + +#rotationSettingsModal .modal-actions button.disable:hover { + background-color: #5c636a; +} + +/* Recommended CSS to make the rotation label visible */ +.status-label { + /* Base styles for all status labels */ + display: inline-block; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.75em; + font-weight: bold; + margin-left: 5px; + vertical-align: middle; + white-space: nowrap; + text-transform: uppercase; +} + +.status-label.valid { + background-color: #28a745; /* Green */ + color: white; +} + +.status-label.invalid { + background-color: #dc3545; /* Red */ + color: white; +} + +.status-label.rotation-enabled { + /* This is the key missing style for the new label */ + background-color: #007bff; /* A distinct blue for rotation status */ + color: white; +} + +/* --- Base Status Label Styles (from previous answers) --- */ +.status-label { + display: inline-block; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.75em; + font-weight: bold; + margin-left: 5px; + vertical-align: middle; + white-space: nowrap; + text-transform: uppercase; +} + +.status-label.valid { + background-color: #28a745; /* Green */ + color: white; +} + +.status-label.invalid { + background-color: #dc3545; /* Red */ + color: white; +} + +.status-label.rotation-enabled { + background-color: #007bff; /* Blue */ + color: white; + position: relative; /* Crucial for positioning the tooltip */ + cursor: help; /* Indicates interactivity */ +} + +/* --- Custom Tooltip Styles --- */ +.has-tooltip::before, +.has-tooltip::after { + --tooltip-bg: #333; + --tooltip-text-color: #fff; + --tooltip-border-radius: 5px; + --tooltip-padding: 8px 12px; + --tooltip-font-size: 0.8em; + --tooltip-max-width: 300px; + --tooltip-offset: 0px; /* Distance from the element */ + + position: absolute; + visibility: hidden; + opacity: 0; + transition: opacity 0.3s ease, visibility 0.3s ease; + z-index: 1000; /* Ensure it's above other content */ + pointer-events: none; /* Allows clicks through tooltip if not interacting with it */ +} + +/* Tooltip Content */ +.has-tooltip::before { + content: attr(data-tooltip); /* Get content from data-tooltip attribute */ + background: var(--tooltip-bg); + color: var(--tooltip-text-color); + border-radius: var(--tooltip-border-radius); + padding: var(--tooltip-padding); + font-size: var(--tooltip-font-size); + max-width: var(--tooltip-max-width); + white-space: pre-wrap; /* Preserve newlines and wrap text */ + text-align: left; /* Align text within the tooltip */ + text-transform: none; /* Don't uppercase tooltip text */ + + /* Positioning - centered horizontally, below the element */ + left: 50%; + transform: translateX(-50%) translateY(var(--tooltip-offset)); + bottom: calc(100% + var(--tooltip-offset)); /* Position above the element by default */ + + /* Ensure it doesn't get squished too thin */ + min-width: 180px; +} + +/* Tooltip Arrow */ +.has-tooltip::after { + content: ''; + border-width: var(--tooltip-offset); + border-style: solid; + border-color: var(--tooltip-bg) transparent transparent transparent; /* Arrow pointing up */ + + /* Positioning - centered horizontally, below the content */ + left: 50%; + transform: translateX(-50%) translateY(var(--tooltip-offset)); + bottom: calc(100% + var(--tooltip-offset) - 5px); /* Position slightly below the content */ + + /* Adjust for arrow direction if position changes */ + /* Example: if tooltip is below, arrow points down */ + /* border-color: transparent transparent var(--tooltip-bg) transparent; */ +} + +/* Show tooltip on hover */ +.has-tooltip:hover::before, +.has-tooltip:hover::after { + visibility: visible; + opacity: 1; +} + +/* Adjust tooltip position if it's too close to the right edge (optional, more complex) */ +/* This is a basic example; for robust positioning, a JS library is best */ +/* @media (max-width: 768px) { + .has-tooltip::before, + .has-tooltip::after { + left: 0; + transform: translateY(var(--tooltip-offset)); + right: auto; + bottom: calc(100% + var(--tooltip-offset)); + } + .has-tooltip::after { + left: 10px; // Adjust arrow + } +} */ \ No newline at end of file diff --git a/static/data_fetchers.js b/static/data_fetchers.js deleted file mode 100755 index 6a2d3ec..0000000 --- a/static/data_fetchers.js +++ /dev/null @@ -1,248 +0,0 @@ -// data_fetchers.js -import { API_BASE_URL, configStore } from './global.js'; -import { showConfigModal, switchTab } from './modals.js'; -import { listListeners } from './listeners.js'; // Will be imported later - -// ========================================================================= -// YAML UTILITY FUNCTION -// ========================================================================= - -/** - * Extracts the YAML section for a specific domain from the filterChains array. - * NOTE: This is a complex utility that you may decide is no longer needed - * given the new approach in showDomainConfig (generating from JSON). - * However, since it was in the original code, we keep it here. - * @param {string} yamlData - The full YAML string containing the listener configuration. - * @param {string} domainName - The domain name to search for. - * @returns {string | null} The YAML string for the matching filterChain. - */ -export function extractFilterChainByDomain(yamlData, domainName) { - if (typeof require === 'undefined' && typeof jsyaml === 'undefined') { - console.error("Error: YAML parser (e.g., js-yaml) is required but not found."); - return null; - } - // ... (rest of the original extractFilterChainByDomain function logic) ... - - let fullConfig; - try { - const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml; - const allDocs = yaml.loadAll(yamlData); - // Find the main configuration object (usually the first object document) - 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; - } - let matchingChain - if (domainName === "*" && fullConfig.filterChains.length > 0) { - matchingChain = fullConfig.filterChains.find(chain => { - return chain.filterChainMatch == null - }); - - } else { - matchingChain = fullConfig.filterChains.find(chain => { - const serverNames = chain.filterChainMatch?.serverNames; - return serverNames && serverNames.includes(domainName); - }); - } - - if (!matchingChain) { - console.log(`No filterChain found for domain: ${domainName}`); - return null; - } - - try { - const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml; - const outputYaml = yaml.dump(matchingChain, { - indent: 2, - lineWidth: -1, - flowLevel: -1 - }); - return outputYaml.trim(); - - } catch (e) { - console.error("Error dumping YAML data:", e); - return null; - } -} - - -// ========================================================================= -// CONFIG-SPECIFIC MODAL LAUNCHERS -// ========================================================================= - -/** - * Handles showing the configuration for an individual FilterChain/Domain. - * This function loads the JSON from memory and generates the YAML from it. - * @param {HTMLElement} element - The DOM element that triggered the function. - */ -export async function showDomainConfig(element) { - const title = element.getAttribute('data-title'); - const listenerName = element.getAttribute('data-listener-name'); - const chainIndex = parseInt(element.getAttribute('data-chain-index'), 10); - - if (!listenerName || isNaN(chainIndex)) { - console.error("Missing required data attributes or invalid chain index for domain config."); - return; - } - - // 1. Get the full Listener JSON config from memory - const listenerConfig = configStore.listeners[listenerName]; - if (!listenerConfig || !listenerConfig.filterChains || !listenerConfig.filterChains[chainIndex]) { - console.error(`Listener or FilterChain at index ${chainIndex} not found in memory for ${listenerName}.`); - showConfigModal(`🚨 Error: Domain Config Not Found`, { name: title, error: `FilterChain index ${chainIndex} not found for listener ${listenerName}.` }, 'Error: JSON configuration missing.'); - return; - } - - // The JSON data for the specific filter chain - const jsonData = listenerConfig.filterChains[chainIndex]; - - // 2. Fetch the full YAML for the listener if not already in memory - let fullListenerYaml = listenerConfig.yaml || 'Loading YAML...'; - if (fullListenerYaml === 'Loading YAML...') { - try { - const response = await fetch(`${API_BASE_URL}/get-listener?name=${listenerName}&format=yaml`); - if (!response.ok) { - fullListenerYaml = `Error fetching YAML: ${response.status} ${response.statusText}`; - } else { - fullListenerYaml = await response.text(); - configStore.listeners[listenerName].yaml = fullListenerYaml; // Store YAML - } - } catch (error) { - console.error("Failed to fetch YAML listener config:", error); - fullListenerYaml = `Network Error fetching YAML: ${error.message}`; - } - } - - let yamlData; - // 3. Extract the specific filterChain YAML using the utility function. - // Use the domain name from the title as a proxy, or the first server_name from the JSON. - const domainName = jsonData.filter_chain_match?.server_names?.[0] || "*"; - - if (fullListenerYaml.startsWith('Error') || fullListenerYaml.startsWith('Network Error')) { - yamlData = fullListenerYaml; // Pass the error message - } else { - // Use the utility function to extract the specific chain's YAML - yamlData = extractFilterChainByDomain(fullListenerYaml, domainName); - if (yamlData === null) { - // As a fallback if the utility fails or doesn't find it, dump the JSON directly - try { - const yaml = (typeof require !== 'undefined') ? require('js-yaml') : jsyaml; - yamlData = yaml.dump(jsonData, { indent: 2, lineWidth: -1, flowLevel: -1 }).trim(); - } catch (e) { - yamlData = `Could not extract or dump YAML for chain: ${domainName}. Error: ${e.message}`; - } - } - } - - // 4. Show the modal - showConfigModal(title, jsonData, yamlData, 'json'); // Default to 'json' since it's guaranteed from memory -} - - -// 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); -// } - -// export async function showListenerConfigModal(listenerName) { -// const config = configStore.listeners[listenerName]; -// if (!config) { -// showConfigModal(`🚨 Error: Listener Not Found`, { name: listenerName, error: 'Configuration data missing from memory.' }, 'Error: Listener not in memory.'); -// return; -// } - -// let yamlData = configStore.listeners[listenerName]?.yaml || 'Loading YAML...'; -// if (yamlData === 'Loading YAML...') { -// try { -// const response = await fetch(`${API_BASE_URL}/get-listener?name=${listenerName}&format=yaml`); -// if (!response.ok) { -// yamlData = `Error fetching YAML: ${response.status} ${response.statusText}`; -// } else { -// yamlData = await response.text(); -// configStore.listeners[listenerName].yaml = yamlData; // Store YAML -// } -// } catch (error) { -// console.error("Failed to fetch YAML listener config:", error); -// yamlData = `Network Error fetching YAML: ${error.message}`; -// } -// } - -// showConfigModal(`Full Config for Listener: ${listenerName}`, config, yamlData); -// } - -// ========================================================================= -// FILTER CHAIN ADDITION LOGIC (NEW) -// ========================================================================= - -/** - * Handles the submission of the new filter chain YAML. - */ -export async function submitNewFilterChain() { - const listenerName = document.getElementById('add-fc-listener-name').value; - const yamlData = document.getElementById('add-fc-yaml-input').value.trim(); - - if (!yamlData) { - alert("Please paste the filter chain YAML configuration."); - return; - } - - if (!listenerName) { - alert("Listener name is missing. Cannot submit."); - return; - } - - const payload = { - listener_name: listenerName, - yaml: yamlData - }; - - try { - const response = await fetch(`${API_BASE_URL}/append-filter-chain`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`HTTP Error ${response.status}: ${errorBody}`); - } - - alert(`Successfully appended new filter chain to '${listenerName}'.`); - - // Close modal and refresh listener list - document.getElementById('addFilterChainModal').style.display = 'none'; - 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}`); - } -} \ No newline at end of file diff --git a/static/data_loader.js b/static/data_loader.js deleted file mode 100755 index d266be7..0000000 --- a/static/data_loader.js +++ /dev/null @@ -1,51 +0,0 @@ -// data_loader.js -import { listClusters } from './clusters.js'; -import { listListeners } from './listeners.js'; -import { listSecrets } from './secrets.js'; -import { listExtensionConfigs } from './extension_configs.js'; -import { setupModalTabs } from './modals.js'; -import {CONSISTENCY_POLL_INTERVAL, checkConsistency} from './global.js'; - - -// ========================================================================= -// COMBINED LOADER & POLLING -// ========================================================================= - -/** - * Loads all primary data (Clusters and Listeners) from the API. - */ -export function loadAllData() { - listClusters(); - listListeners(); - listSecrets(); - listExtensionConfigs(); -} - - -// ========================================================================= -// INITIALIZATION -// ========================================================================= - -window.onload = () => { - loadAllData(); - setupModalTabs(); // Setup tab logic on load - checkConsistency(); // Initial check - setInterval(checkConsistency, CONSISTENCY_POLL_INTERVAL); // Start polling -}; - - -// ========================================================================= -// EXPOSE GLOBAL FUNCTIONS (Required for inline HTML handlers) -// ========================================================================= - -// This section is necessary if your HTML uses inline onclick attributes, -// as is common in older or simpler JS projects. -import * as Modals from './modals.js'; -import * as Fetchers from './data_fetchers.js'; -import * as Clusters from './clusters.js'; -import * as Listeners from './listeners.js'; -import * as ExtensionConfig from './extension_configs.js'; -import * as Consistency from './consistency.js'; - -// Attach all necessary functions to the global window object -Object.assign(window, Modals, Fetchers, Clusters, Listeners,ExtensionConfig, Consistency); \ No newline at end of file diff --git a/static/extension_configs.js b/static/extension_configs.js deleted file mode 100755 index c627a4c..0000000 --- a/static/extension_configs.js +++ /dev/null @@ -1,237 +0,0 @@ -// extension_configs.js -import { API_BASE_URL, configStore, cleanupConfigStore } from './global.js'; - -// ========================================================================= -// EXTENSION CONFIG UTILITIES -// ========================================================================= - -/** - * Extracts the Type URL of the inner, configured resource from the TypedExtensionConfig. - * @param {object} extensionConfig - The ExtensionConfig object from the API. - * @returns {string} The inner type URL or a default message. - */ -function getExtensionConfigTypeUrl(extensionConfig) { - try { - const typeUrl = extensionConfig.typed_config.type_url; - if (typeUrl) { - // Display only the last part of the type URL (e.g., Lua or JwtAuthn) - return typeUrl.substring(typeUrl.lastIndexOf('/') + 1); - } - return '(Unknown Type)'; - } catch { - return '(Config Error)'; - } -} - -// ========================================================================= -// EXTENSION CONFIG CORE LOGIC -// ========================================================================= - -/** - * Fetches and lists all enabled and disabled ExtensionConfigs. - */ -export async function listExtensionConfigs() { - const tableBody = document.getElementById('extensionconfig-table-body'); - if (!tableBody) { - console.error("Could not find element with ID 'extensionconfig-table-body'."); - return; - } - - tableBody.innerHTML = - 'Loading...'; - - try { - const response = await fetch(`${API_BASE_URL}/list-extensionconfigs`); - if (!response.ok) throw new Error(response.statusText); - - const extensionConfigResponse = await response.json(); - - const allConfigs = [ - ...(extensionConfigResponse.enabled || []).map(c => ({ ...c, status: 'Enabled', configData: c })), - ...(extensionConfigResponse.disabled || []).map(c => ({ ...c, status: 'Disabled', configData: c })) - ]; - - if (!allConfigs.length) { - tableBody.innerHTML = - 'No ExtensionConfigs found.'; - configStore.extension_configs = {}; - return; - } - cleanupConfigStore(); - - // Store full configs in memory by name - configStore.extension_configs = allConfigs.reduce((acc, c) => { - const existingYaml = acc[c.name]?.yaml; - acc[c.name] = { ...c.configData, yaml: existingYaml }; - return acc; - }, configStore.extension_configs); - - - tableBody.innerHTML = ''; - allConfigs.forEach(config => { - const row = tableBody.insertRow(); - if (config.status === 'Disabled') row.classList.add('disabled-row'); - - let actionButtons = ''; - if (config.status === 'Enabled') { - actionButtons = ``; - } else { - // When disabled, show Enable and Remove buttons - actionButtons = ` - - - `; - } - - // Config Name Hyperlink (uses showExtensionConfigModal from global.js - must be implemented there) - const nameCell = row.insertCell(); - nameCell.innerHTML = - `${config.name}`; - - row.insertCell().textContent = config.status; - row.insertCell().innerHTML = getExtensionConfigTypeUrl(config); // Shows the inner type - row.insertCell().innerHTML = actionButtons; - }); - } catch (error) { - tableBody.innerHTML = `🚨 ExtensionConfig Error: ${error.message}`; - console.error("ExtensionConfig Fetch/Parse Error:", error); - } -} - -// ========================================================================= -// EXTENSION CONFIG ENABLE/DISABLE/REMOVE LOGIC -// ========================================================================= - -async function toggleExtensionConfigStatus(configName, action) { - let url = (action === 'remove') ? `${API_BASE_URL}/remove-extensionconfig` : `${API_BASE_URL}/${action}-extensionconfig`; - const payload = { name: configName }; - - 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(`ExtensionConfig '${configName}' successfully ${action}d.`); - cleanupConfigStore(); - listExtensionConfigs(); - } catch (error) { - console.error(`Failed to ${action} ExtensionConfig '${configName}':`, error); - alert(`Failed to ${action} ExtensionConfig '${configName}'. Check console for details.`); - } -} - -// Attach functions to the global window object so they can be called from HTML buttons -export function disableExtensionConfig(configName, event) { - event.stopPropagation(); - if (confirm(`Are you sure you want to DISABLE ExtensionConfig: ${configName}?`)) { - toggleExtensionConfigStatus(configName, 'disable'); - } -} - -export function enableExtensionConfig(configName, event) { - event.stopPropagation(); - if (confirm(`Are you sure you want to ENABLE ExtensionConfig: ${configName}?`)) { - toggleExtensionConfigStatus(configName, 'enable'); - } -} - -export function removeExtensionConfig(configName, event) { - event.stopPropagation(); - if (confirm(`⚠️ WARNING: Are you absolutely sure you want to PERMANENTLY REMOVE ExtensionConfig: ${configName}? This action cannot be undone.`)) { - toggleExtensionConfigStatus(configName, 'remove'); - } -} - -// ========================================================================= -// ADD EXTENSION CONFIG LOGIC -// ========================================================================= - -/** - * Shows the modal for adding a new ExtensionConfig. - */ -export function showAddExtensionConfigModal() { - document.getElementById('add-extension-config-yaml-input').value = ''; - // Clear checkbox on show - const upsertCheckbox = document.getElementById('add-extension-config-upsert-flag'); - if (upsertCheckbox) { - upsertCheckbox.checked = false; - } - document.getElementById('addExtensionConfigModal').style.display = 'block'; -} - -/** - * Hides the modal for adding a new ExtensionConfig. - */ -export function hideAddExtensionConfigModal() { - const modal = document.getElementById('addExtensionConfigModal'); - if (modal) { - modal.style.display = 'none'; - document.getElementById('add-extension-config-yaml-input').value = ''; - // Clear checkbox on hide - const upsertCheckbox = document.getElementById('add-extension-config-upsert-flag'); - if (upsertCheckbox) { - upsertCheckbox.checked = false; - } - } -} - - -/** - * Submits the new ExtensionConfig YAML to the /add-extensionconfig endpoint. - */ -export async function submitNewExtensionConfig() { - const yamlInput = document.getElementById('add-extension-config-yaml-input'); - const upsertCheckbox = document.getElementById('add-extension-config-upsert-flag'); - const configYaml = yamlInput.value.trim(); - - if (!configYaml) { - alert('Please paste the ExtensionConfig YAML configuration.'); - return; - } - - try { - const payload = { yaml: configYaml }; - - // Add upsert flag to payload if checkbox is checked - if (upsertCheckbox && upsertCheckbox.checked) { - payload.upsert = true; - } - - const url = `${API_BASE_URL}/add-extensionconfig`; - - 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 ExtensionConfig successfully added.`); - alert('ExtensionConfig successfully added! The dashboard will now refresh.'); - - yamlInput.value = ''; - // Uncheck the box upon success/closing - if (upsertCheckbox) { - upsertCheckbox.checked = false; - } - hideAddExtensionConfigModal(); - - cleanupConfigStore(); - listExtensionConfigs(); - - } catch (error) { - console.error(`Failed to add new ExtensionConfig:`, error); - alert(`Failed to add new ExtensionConfig. Check console for details. Error: ${error.message}`); - } -} \ No newline at end of file diff --git a/static/global.js b/static/global.js deleted file mode 100755 index 3a1b447..0000000 --- a/static/global.js +++ /dev/null @@ -1,464 +0,0 @@ -// 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); -}; \ No newline at end of file diff --git a/static/index.html b/static/index.html index 73b21e2..3f16cf0 100755 --- a/static/index.html +++ b/static/index.html @@ -5,8 +5,9 @@ Envoy Configuration Dashboard - + + @@ -38,7 +39,8 @@ Cluster - + +

Existing Listeners (Click a domain/filter for details)

@@ -120,36 +122,38 @@ - + -