<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Envoy Filter Chain Configurator</title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> <style> /* Custom styles for better readability and structure */ body { font-family: "Inter", sans-serif; background-color: #f0f2f5; /* Lighter background matching dashboard */ color: #333; /* Darker text for readability */ } #app-container { max-width: 1000px; /* Slightly wider to match dashboard feel */ background-color: #ffffff; /* Removed shadow-2xl and rounded-xl for a flatter, more integrated look */ /* padding stays generous for content spacing */ border-radius: 0.5rem; /* Subtle rounding like the table in the screenshot */ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); /* Subtle shadow like the table */ } #yaml-output { background-color: #1f2937; /* Darker background for code */ color: #e5e7eb; /* Lighter text for code */ border: 1px solid #4b5563; /* Darker border for contrast */ font-family: 'Consolas', 'Monaco', monospace; line-height: 1.4; min-height: 400px; padding: 15px; box-shadow: none; /* No extra shadow on code output */ border-radius: 0.5rem; /* Consistent border radius */ } #yaml-output:disabled { cursor: default; } .optional-fields { transition: all 0.3s ease-in-out; border: 1px solid #e5e7eb; /* Subtle border for optional fields */ background-color: #f9fafb; /* Very light background */ border-radius: 0.5rem; } /* Disable styling for Simple Mode inputs when Advanced Mode is on */ .simple-disabled { opacity: 0.5; /* Slightly less opaque */ pointer-events: none; transition: opacity 0.3s ease; } /* Custom toggle switch styling for Advanced Mode */ #advanced-mode-toggle:checked ~ .block { background-color: #16a34a; /* A shade of green matching common success states */ } #advanced-mode-toggle:checked ~ .dot { transform: translateX(100%); } /* Hide the button since generation is now real-time */ #generate-button { display: none; } /* Style adjustments for better form readability */ input[type="text"], textarea { border: 1px solid #d1d5db; /* Lighter, more subtle border */ background-color: #ffffff; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; border-radius: 0.375rem; /* Slightly smaller border radius for inputs */ } input[type="checkbox"] { border-color: #d1d5db; color: #2563eb; /* Blue checkbox accent */ } input[type="text"]:focus, textarea:focus { border-color: #3b82f6; /* Blue focus ring */ box-shadow: 0 0 0 1px #3b82f6; /* Subtle focus ring */ } /* Adjustments for button styles */ .btn-primary { background-color: #2563eb; /* Blue from dashboard button */ color: #ffffff; font-weight: 500; /* Medium font weight */ border-radius: 0.375rem; padding: 0.625rem 1rem; /* Adjust padding */ } .btn-primary:hover { background-color: #1d4ed8; /* Darker blue on hover */ } .btn-secondary-outline { background-color: transparent; color: #4b5563; /* Darker grey text */ border: 1px solid #d1d5db; font-weight: 500; border-radius: 0.375rem; padding: 0.625rem 1rem; } .btn-secondary-outline:hover { background-color: #f3f4f6; border-color: #9ca3af; } .remove-domain-btn { color: #9ca3af; /* Grey for delete icon */ border: none; background-color: transparent; } .remove-domain-btn:hover { color: #ef4444; /* Red on hover */ } </style> <script> tailwind.config = { theme: { extend: { borderRadius: { 'xl': '0.75rem', } } } } </script> </head> <body class="p-4 sm:p-8"> <div id="app-container" class="mx-auto bg-white p-6 md:p-10"> <h1 class="text-2xl font-semibold text-gray-800 mb-6 pb-4 border-b border-gray-200 flex items-center"> Envoy Filter Chain Composer <span class="ml-2 text-gray-400 text-base font-normal"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> </svg> </span> </h1> <p class="text-gray-600 mb-8">Configure your Envoy filter chain for a Listener using simple inputs or switch to advanced editing mode.</p> <div class="space-y-6" id="simple-mode-inputs"> <div class="grid md:grid-cols-2 gap-6"> <div class="md:col-span-2"> <label class="block text-sm font-medium text-gray-700 mb-2">Service Domains (Server Names):</label> <div id="domain-inputs-container" class="space-y-3"> <div class="flex items-center space-x-2 domain-group"> <input type="text" id="domain" value="audio.jerxie.com" data-is-primary="true" class="domain-input block w-full rounded-lg border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2.5 border" placeholder="Primary Domain (e.g., api.example.com)"> <button type="button" class="remove-domain-btn p-2 opacity-0 cursor-default" disabled> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg> </button> </div> </div> <button type="button" id="add-domain-button" class="mt-3 btn-secondary-outline flex items-center" onclick="addDomainInput()"> <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg> Add Domain </button> </div> </div> <div> <label for="service-name" class="block text-sm font-medium text-gray-700">Cluster Name (e.g., _pcb_server):</label> <input type="text" id="service-name" value="_nas_audio" class="block w-full rounded-lg border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2.5 border"> </div> <div class="flex flex-col sm:flex-row gap-6 p-4 bg-blue-50 rounded-lg border border-blue-200"> <div class="flex items-center"> <input id="enable-websocket" type="checkbox" class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"> <label for="enable-websocket" class="ml-2 block text-sm font-medium text-gray-700 select-none">Enable WebSocket Upgrade</label> </div> <div class="flex items-center"> <input id="enable-tls" type="checkbox" checked class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"> <label for="enable-tls" class="ml-2 block text-sm font-medium text-gray-700 select-none">Enable TLS (HTTPS)</label> </div> </div> <div id="tls-fields" class="optional-fields p-4"> <h3 class="text-base font-semibold text-gray-800 mb-3">TLS Certificate Paths</h3> <p class="text-xs text-gray-500 mb-4">The 'DOMAIN_PLACEHOLDER' will be replaced by your **Primary Domain**.</p> <div class="space-y-4"> <div> <label for="cert-chain" class="block text-sm font-medium text-gray-700">Certificate Chain Path:</label> <input type="text" id="cert-chain" value="/etc/certs/downstream/DOMAIN_PLACEHOLDER/fullchain.pem" class="block w-full rounded-lg border-gray-300 shadow-sm p-2 border text-sm"> </div> <div> <label for="private-key" class="block text-sm font-medium text-gray-700">Private Key Path:</label> <input type="text" id="private-key" value="/etc/certs/downstream/DOMAIN_PLACEHOLDER/privkey.pem" class="block w-full rounded-lg border-gray-300 shadow-sm p-2 border text-sm"> </div> </div> </div> </div> <div class="mt-8 flex items-center justify-between p-4 bg-gray-50 rounded-lg border border-gray-200"> <h2 class="text-base font-semibold text-gray-800">Advanced Editing Mode</h2> <label for="advanced-mode-toggle" class="flex items-center cursor-pointer select-none"> <span class="text-sm font-medium text-gray-600 mr-3">Manual YAML Override</span> <div class="relative"> <input type="checkbox" id="advanced-mode-toggle" class="sr-only"> <div class="block bg-gray-400 w-12 h-6 rounded-full transition duration-300"></div> <div class="dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition transform duration-300"></div> </div> </label> </div> <div class="flex justify-between items-center mt-10 mb-4"> <h2 class="text-xl font-semibold text-gray-800">Composed YAML Output</h2> <button id="copy-yaml-button" class="btn-primary flex items-center" onclick="copyYamlToClipboard()"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2.25A2.25 2.25 0 0121 7.25v2.25m-2.25 0v5.5A2.25 2.25 0 0118.75 17H5.25A2.25 2.25 0 013 14.75v-5.5m1.5-3.75h5.5" ></path></svg> Copy YAML </button> </div> <textarea id="yaml-output" rows="25" class="block w-full rounded-lg border-gray-300 p-4 font-mono text-sm resize-none"></textarea> </div> <script> // Global flag and cache for simple mode inputs let isAdvancedMode = false; let simpleInputs = []; // --- Domain Management Functions --- function getDomainInputs() { // Get all inputs with the class 'domain-input', filter out empty values, and trim return Array.from(document.querySelectorAll('#domain-inputs-container .domain-input')) .map(input => input.value.trim()) .filter(value => value.length > 0); } function addDomainInput() { const container = document.getElementById('domain-inputs-container'); const newDomainGroup = document.createElement('div'); newDomainGroup.className = 'flex items-center space-x-2 domain-group'; newDomainGroup.innerHTML = ` <input type="text" value="" class="domain-input block w-full rounded-lg border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2.5 border" placeholder="Additional Domain (e.g., api.local)"> <button type="button" onclick="removeDomainInput(this)" class="remove-domain-btn p-2 transition duration-150"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg> </button> `; const newInput = newDomainGroup.querySelector('.domain-input'); container.appendChild(newDomainGroup); // Attach the event listener to the new input newInput.addEventListener('input', generateYaml); // Re-populate the simpleInputs list for toggleAdvancedMode to work fully populateSimpleInputs(); // Focus the new input newInput.focus(); generateYaml(); // Regenerate YAML after adding a new input } function removeDomainInput(buttonElement) { const domainGroup = buttonElement.closest('.domain-group'); if (domainGroup) { domainGroup.remove(); populateSimpleInputs(); // Update the simpleInputs list generateYaml(); // Regenerate YAML after removing an input } } // --- Utility Function: Copy to Clipboard --- async function copyYamlToClipboard() { const outputElement = document.getElementById('yaml-output'); const copyButton = document.getElementById('copy-yaml-button'); const originalText = copyButton.innerHTML; // Get full HTML including SVG try { // Use the modern clipboard API await navigator.clipboard.writeText(outputElement.value); // Provide visual feedback copyButton.innerHTML = `<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> Copied!`; copyButton.classList.add('bg-green-600', 'hover:bg-green-700'); copyButton.classList.remove('bg-blue-600', 'hover:bg-blue-700'); // Revert button text after a delay setTimeout(() => { copyButton.innerHTML = originalText; copyButton.classList.remove('bg-green-600', 'hover:bg-green-700'); copyButton.classList.add('bg-blue-600', 'hover:bg-blue-700'); }, 2000); } catch (err) { console.error('Failed to copy YAML: ', err); alert('Failed to copy YAML. Your browser might require user interaction (e.g., focusing the textarea) for clipboard access.'); } } // --- Core YAML Generation Logic --- function generateYaml() { // Stop automatic generation if user is manually editing if (isAdvancedMode) { return; } const allDomains = getDomainInputs(); const primaryDomain = allDomains[0]; // Used for cert path substitution const serviceName = document.getElementById('service-name').value.trim(); const enableWebsocket = document.getElementById('enable-websocket').checked; const enableTls = document.getElementById('enable-tls').checked; const certChain = document.getElementById('cert-chain').value.trim(); const privateKey = document.getElementById('private-key').value.trim(); const outputElement = document.getElementById('yaml-output'); outputElement.value = 'Generating...'; if (!primaryDomain || !serviceName) { outputElement.value = 'ERROR: At least one Domain and Cluster Name are required.'; return; } // Generate the list of YAML entries for domains // serverNames: starts at 2 spaces, so domains need 4 spaces const serverNamesEntries = allDomains.map(d => ` - ${d}`).join('\n'); // virtualHosts: domains: starts at 10 spaces, so domains need 12 spaces const routeDomainEntries = allDomains.map(d => ` - ${d}`).join('\n'); // --- 1. Base YAML Structure (Rooted at filterChainMatch/filters) --- let yaml = `filterChainMatch: serverNames: ${serverNamesEntries} filters: - name: envoy.filters.network.http_connection_manager typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager httpFilters: - name: envoy.filters.http.router typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router routeConfig: virtualHosts: - domains: ${routeDomainEntries} name: ${serviceName.replace(/[^a-zA-Z0-9_]/g, '').toLowerCase()}_host routes: - match: prefix: / route: cluster: ${serviceName} timeout: 0s statPrefix: ingress_http`; // --- 2. WebSocket Configuration (Optional) --- if (enableWebsocket) { // upgradeConfigs: starts at 6 spaces yaml += ` upgradeConfigs: - upgradeType: websocket`; } // --- 3. TLS Configuration (Optional) --- if (enableTls) { const resolvedCertChain = certChain.replace(/DOMAIN_PLACEHOLDER/g, primaryDomain); const resolvedPrivateKey = privateKey.replace(/DOMAIN_PLACEHOLDER/g, primaryDomain); if (!resolvedCertChain || !resolvedPrivateKey) { outputElement.value = 'ERROR: TLS is enabled, but Certificate Chain and Private Key paths are required.'; return; } // transportSocket: is at root level yaml += ` transportSocket: name: envoy.transport_sockets.tls typedConfig: '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext commonTlsContext: tlsCertificates: - certificateChain: filename: ${resolvedCertChain} privateKey: filename: ${resolvedPrivateKey}`; } // Final Output outputElement.value = yaml.trim(); } // --- Advanced Mode Toggle Logic --- function toggleAdvancedMode(isTogglingOn) { const yamlOutput = document.getElementById('yaml-output'); const simpleInputsContainer = document.getElementById('simple-mode-inputs'); isAdvancedMode = isTogglingOn; if (isTogglingOn) { // SWITCHING TO ADVANCED MODE (Editable Output, Disabled Inputs) // 1. Disable simple inputs visually and functionally simpleInputsContainer.classList.add('simple-disabled'); simpleInputs.forEach(input => { input.disabled = true; }); // 2. Enable the YAML output area for editing yamlOutput.disabled = false; yamlOutput.focus(); } else { // SWITCHING TO SIMPLE MODE (Read-only Output, Enabled Inputs) // 1. Enable simple inputs visually and functionally simpleInputsContainer.classList.remove('simple-disabled'); simpleInputs.forEach(input => { input.disabled = false; }); // 2. Disable the YAML output area (it becomes the display) yamlOutput.disabled = true; // 3. IMPORTANT: Regenerate YAML immediately to overwrite any manual changes generateYaml(); } } // --- Populate Inputs for Advanced Mode Toggle --- function populateSimpleInputs() { // Re-select all relevant inputs every time a domain is added/removed simpleInputs = [ ...Array.from(document.querySelectorAll('#domain-inputs-container .domain-input')), document.getElementById('service-name'), document.getElementById('enable-websocket'), document.getElementById('enable-tls'), document.getElementById('cert-chain'), document.getElementById('private-key') ].filter(Boolean); // Filter out any null elements } // --- Event Listeners for UI interaction --- document.addEventListener('DOMContentLoaded', function() { const advancedModeToggle = document.getElementById('advanced-mode-toggle'); const tlsFields = document.getElementById('tls-fields'); const enableTlsCheckbox = document.getElementById('enable-tls'); // 1. Initial population of the simpleInputs list populateSimpleInputs(); // 2. Attach generateYaml to all initial inputs (domain inputs handled dynamically) simpleInputs.forEach(input => { // Skip domain-inputs here, as their listeners are managed in addDomainInput if (!input.classList.contains('domain-input')) { const eventType = (input.type === 'checkbox' || input.tagName === 'SELECT') ? 'change' : 'input'; input.addEventListener(eventType, generateYaml); } }); // 5. Update domain placeholders in TLS paths (only tied to the primary domain) const updateTlsPathsAndGenerateYaml = () => { const primaryDomainInput = document.getElementById('domain'); if (primaryDomainInput) { // Ensure primaryDomainInput exists const newDomain = primaryDomainInput.value.trim(); document.getElementById('cert-chain').value = `/etc/certs/downstream/${newDomain}/fullchain.pem`; document.getElementById('private-key').value = `/etc/certs/downstream/${newDomain}/privkey.pem`; } generateYaml(); }; // Call this once on load to set initial paths and generate YAML updateTlsPathsAndGenerateYaml(); // Re-attach listeners for initial domain inputs specifically document.querySelectorAll('#domain-inputs-container .domain-input').forEach(input => { input.addEventListener('input', updateTlsPathsAndGenerateYaml); }); // 3. Advanced Mode Toggle Listener advancedModeToggle.addEventListener('change', function() { toggleAdvancedMode(this.checked); }); // 4. TLS Fields Visibility Listener enableTlsCheckbox.addEventListener('change', function() { tlsFields.classList.toggle('hidden', !this.checked); generateYaml(); }); // Initial setup for Advanced Mode (default to simple) toggleAdvancedMode(false); }); </script> </body> </html>