Newer
Older
EnvoyControlPlane / static / clusters.js
// clusters.js
import { API_BASE_URL, configStore, cleanupConfigStore } from './global.js';

// =========================================================================
// CLUSTER UTILITIES
// =========================================================================

function getClusterEndpointDetails(cluster) {
    try {
        const endpoints = cluster.load_assignment?.endpoints;
        if (!endpoints?.length) return '<span style="color: gray;">(No Endpoints)</span>';
        const lbEndpoints = endpoints[0].lb_endpoints;
        if (!lbEndpoints?.length) return '<span style="color: gray;">(No LB Endpoints)</span>';

        // 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 ? '<span class="tls-badge">TLS/SSL</span>' : '';
        return `${address}:${port} ${tls}`;
    } catch {
        return '<span style="color: gray;">(Config Error)</span>';
    }
}

// =========================================================================
// CLUSTER CORE LOGIC
// =========================================================================

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 =
        '<tr><td colspan="5" style="text-align: center; padding: 20px;">Loading...</td></tr>';

    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 =
                '<tr><td colspan="5" style="text-align: center; color: var(--secondary-color);">No clusters found.</td></tr>';
            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 = `<button class="action-button disable" onclick="window.disableCluster('${cluster.name}', event)">Disable</button>`;
            } else {
                // When disabled, show Enable and Remove buttons
                actionButtons = `
                    <button class="action-button enable" onclick="window.enableCluster('${cluster.name}', event)">Enable</button>
                    <button class="action-button remove" onclick="window.removeCluster('${cluster.name}', event)">Remove</button>
                `;
            }

            // Cluster Name Hyperlink (uses showClusterConfigModal from global.js)
            const clusterNameCell = row.insertCell();
            clusterNameCell.innerHTML = 
                `<a href="#" onclick="event.preventDefault(); window.showClusterConfigModal('${cluster.name}')"><span class="cluster-name">${cluster.name}</span></a>`;
            
            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 = `<tr><td colspan="5" class="error" style="text-align: center;">🚨 Cluster Error: ${error.message}</td></tr>`;
        console.error("Cluster Fetch/Parse Error:", error);
    }
}


// =========================================================================
// CLUSTER ENABLE/DISABLE/REMOVE LOGIC
// =========================================================================

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 = '';
    document.getElementById('addClusterModal').style.display = 'block';
}

/**
 * Hides the modal for adding a new cluster.
 */
export function hideAddClusterModal() {
    const modal = document.getElementById('addClusterModal');
    if (modal) {
        modal.style.display = 'none';
        document.getElementById('add-cluster-yaml-input').value = ''; 
    }
}


/**
 * Submits the new cluster YAML to the /add-cluster endpoint.
 */
export async function submitNewCluster() {
    const yamlInput = document.getElementById('add-cluster-yaml-input');
    const clusterYaml = yamlInput.value.trim();

    if (!clusterYaml) {
        alert('Please paste the cluster YAML configuration.');
        return;
    }

    try {
        const payload = { yaml: clusterYaml };
        const url = `${API_BASE_URL}/add-cluster`;
        
        const response = await fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });

        if (!response.ok) {
            const errorBody = await response.text();
            throw new Error(`HTTP Error ${response.status}: ${errorBody}`);
        }

        console.log(`New cluster successfully added.`);
        alert('Cluster successfully added! The dashboard will now refresh.');
        
        yamlInput.value = '';
        hideAddClusterModal(); 
        
        cleanupConfigStore();
        listClusters(); 

    } catch (error) {
        console.error(`Failed to add new cluster:`, error);
        alert(`Failed to add new cluster. Check console for details. Error: ${error.message}`);
    }
}