// Configuration Management Component - Agent haproxy.cfg viewer import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Card, Table, Button, Space, Tag, message, Badge, Typography, Alert, Tooltip, Spin, Modal, Input, Progress, theme } from 'antd'; import { DesktopOutlined, EyeOutlined, DownloadOutlined, ReloadOutlined, HeartTwoTone, WarningOutlined, CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined, LinuxOutlined, AppleOutlined, WifiOutlined, DisconnectOutlined, InfoCircleOutlined, FileTextOutlined, SyncOutlined } from '@ant-design/icons'; import axios from 'axios'; import { useCluster } from '../contexts/ClusterContext'; import { extractApiError } from '../utils/apiError'; const { Text, Title, Paragraph } = Typography; const { TextArea } = Input; const Configuration = () => { // === State Management === const [agents, setAgents] = useState([]); const [filteredAgents, setFilteredAgents] = useState([]); const [searchText, setSearchText] = useState(''); const [loading, setLoading] = useState(false); const [configModalVisible, setConfigModalVisible] = useState(false); const [selectedAgent, setSelectedAgent] = useState(null); const [configContent, setConfigContent] = useState(''); const [configLoading, setConfigLoading] = useState(false); const [requestId, setRequestId] = useState(null); const [progressPercent, setProgressPercent] = useState(0); const [timeRemaining, setTimeRemaining] = useState(60); // Use refs to store interval and timeout IDs to avoid stale closures const pollingIntervalRef = useRef(null); const timeoutRef = useRef(null); const pollCountRef = useRef(0); const { selectedCluster, loading: clustersLoading } = useCluster(); const { token } = theme.useToken(); // Platform configuration const platformConfig = { linux: { name: 'Linux', icon: , color: '#1890ff' }, darwin: { name: 'macOS', icon: , color: '#722ed1' } }; // Fetch agents from API const fetchAgents = useCallback(async () => { if (!selectedCluster) { setAgents([]); setFilteredAgents([]); return; } setLoading(true); try { const params = { pool_id: selectedCluster.pool_id }; const response = await axios.get('/api/agents', { params, timeout: 10000, headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' } }); const agentsData = response.data.agents || []; setAgents(agentsData); setFilteredAgents(agentsData); } catch (error) { console.error('Failed to fetch agents:', error); message.error('Failed to fetch agents: ' + (extractApiError(error, error.message))); } finally { setLoading(false); } }, [selectedCluster]); // Search/filter agents const handleSearch = (value) => { setSearchText(value); if (!value) { setFilteredAgents(agents); } else { const filtered = agents.filter(agent => agent.name.toLowerCase().includes(value.toLowerCase()) || agent.hostname?.toLowerCase().includes(value.toLowerCase()) || agent.pool_name?.toLowerCase().includes(value.toLowerCase()) || agent.platform?.toLowerCase().includes(value.toLowerCase()) || agent.ip_address?.toLowerCase().includes(value.toLowerCase()) ); setFilteredAgents(filtered); } }; // Update filteredAgents when agents change useEffect(() => { handleSearch(searchText); }, [agents, searchText]); // CRITICAL FIX: Clear agents immediately when cluster changes (prevent cache/mixing) useEffect(() => { if (selectedCluster) { console.log(`🔄 CLUSTER CHANGED: ${selectedCluster.name} (ID: ${selectedCluster.id}) - Clearing agents to prevent mixing...`); setAgents([]); setFilteredAgents([]); } }, [selectedCluster?.id]); // Trigger on cluster ID change only // Initial load useEffect(() => { fetchAgents(); }, [fetchAgents, selectedCluster]); // Get platform icon const getPlatformIcon = (platform) => { return platformConfig[platform]?.icon || ; }; // Enhanced status badge const getStatusBadge = (health, status, lastSeen) => { const statusConfig = { healthy: { status: 'success', icon: , text: 'Online', color: '#52c41a' }, warning: { status: 'warning', icon: , text: 'Warning', color: '#faad14' }, offline: { status: 'error', icon: , text: 'Offline', color: '#ff4d4f' }, unknown: { status: 'default', icon: , text: 'Unknown', color: '#d9d9d9' } }; const config = statusConfig[health] || statusConfig.unknown; const getLastSeenText = () => { if (!lastSeen) return 'Never connected'; const date = new Date(lastSeen); const now = new Date(); const diffMinutes = Math.floor((now - date) / (1000 * 60)); if (diffMinutes < 1) return 'Just now'; if (diffMinutes < 60) return `${diffMinutes}m ago`; if (diffMinutes < 1440) return `${Math.floor(diffMinutes / 60)}h ago`; return `${Math.floor(diffMinutes / 1440)}d ago`; }; return ( ); }; // Cancel config request const cancelConfigRequest = () => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setConfigLoading(false); setProgressPercent(0); setTimeRemaining(60); pollCountRef.current = 0; message.info('Configuration request cancelled'); }; // Poll for config response const pollConfigResponse = useCallback(async (reqId) => { try { pollCountRef.current += 1; const response = await axios.get(`/api/configuration/response/${reqId}`); const data = response.data; if (data.status === 'completed' && data.config_content) { // Success - got the config if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setConfigLoading(false); setProgressPercent(100); setConfigContent(data.config_content); setConfigModalVisible(true); message.success('Configuration retrieved successfully'); } else if (data.status === 'expired') { // Request expired if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setConfigLoading(false); setProgressPercent(0); message.error('Request expired. Agent did not respond in time.'); } else { // Still pending - update progress const elapsed = pollCountRef.current * 2; // 2 seconds per poll const progress = Math.min((elapsed / 90) * 100, 95); // Cap at 95% until complete (90s timeout) setProgressPercent(progress); } } catch (error) { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setConfigLoading(false); setProgressPercent(0); message.error('Failed to get configuration: ' + (extractApiError(error, error.message))); } }, []); // View configuration const viewConfig = async (agent) => { if (agent.status !== 'online') { message.warning(`Agent '${agent.name}' is not online. Cannot retrieve configuration.`); return; } if (!selectedCluster) { message.error('No cluster selected. Please select a cluster first.'); return; } setSelectedAgent(agent); setConfigLoading(true); setConfigContent(''); setProgressPercent(0); setTimeRemaining(90); pollCountRef.current = 0; try { // Create config request with cluster_id const response = await axios.post('/api/configuration/request', null, { params: { agent_name: agent.name, cluster_id: selectedCluster.id, request_type: 'view' } }); const reqId = response.data.request_id; setRequestId(reqId); message.info('Configuration request sent. Waiting for agent response...'); // Start polling for response pollingIntervalRef.current = setInterval(() => { pollConfigResponse(reqId); }, 2000); // Poll every 2 seconds // Timeout after 90 seconds (agent checks every 30 seconds, worst case ~60s with restart) timeoutRef.current = setTimeout(() => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } setConfigLoading(false); setProgressPercent(0); message.error('Request timeout. Agent did not respond in time. Please try again.'); }, 90000); // 90 seconds } catch (error) { setConfigLoading(false); setProgressPercent(0); message.error('Failed to create configuration request: ' + (extractApiError(error, error.message))); } }; // Download configuration const downloadConfig = async (agent) => { if (agent.status !== 'online') { message.warning(`Agent '${agent.name}' is not online. Cannot retrieve configuration.`); return; } if (!selectedCluster) { message.error('No cluster selected. Please select a cluster first.'); return; } setSelectedAgent(agent); setConfigLoading(true); setProgressPercent(0); pollCountRef.current = 0; try { // Create config request with cluster_id const response = await axios.post('/api/configuration/request', null, { params: { agent_name: agent.name, cluster_id: selectedCluster.id, request_type: 'download' } }); const reqId = response.data.request_id; message.info('Configuration request sent. Waiting for agent response...'); // Start polling for response const pollForDownload = async () => { try { pollCountRef.current += 1; const elapsed = pollCountRef.current * 2; const progress = Math.min((elapsed / 90) * 100, 95); // 90s timeout setProgressPercent(progress); const pollResponse = await axios.get(`/api/configuration/response/${reqId}`); const data = pollResponse.data; if (data.status === 'completed' && data.config_content) { // Success - download the config setProgressPercent(100); if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } const element = document.createElement('a'); const file = new Blob([data.config_content], { type: 'text/plain' }); element.href = URL.createObjectURL(file); element.download = `${agent.name}-haproxy.cfg`; document.body.appendChild(element); element.click(); document.body.removeChild(element); setConfigLoading(false); setProgressPercent(0); message.success('Configuration downloaded successfully'); return true; } else if (data.status === 'expired') { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setConfigLoading(false); setProgressPercent(0); message.error('Request expired. Agent did not respond in time.'); return true; } return false; } catch (error) { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } setConfigLoading(false); setProgressPercent(0); message.error('Failed to download configuration: ' + (extractApiError(error, error.message))); return true; } }; // Poll every 2 seconds pollingIntervalRef.current = setInterval(async () => { await pollForDownload(); }, 2000); // Timeout after 90 seconds (agent checks every 30 seconds, worst case ~60s with restart) timeoutRef.current = setTimeout(() => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } setConfigLoading(false); setProgressPercent(0); message.error('Request timeout. Agent did not respond in time. Please try again.'); }, 90000); // 90 seconds } catch (error) { setConfigLoading(false); setProgressPercent(0); message.error('Failed to create configuration request: ' + (extractApiError(error, error.message))); } }; // Cleanup polling on unmount useEffect(() => { return () => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }; }, []); // Table columns const columns = [ { title: 'Agent Info', key: 'agent_info', render: (text, record) => ( {getPlatformIcon(record.platform)} {record.name} {record.health === 'healthy' && ( )} {record.hostname || 'Unknown hostname'} {record.ip_address && ( IP: {record.ip_address} )} ), width: 220, }, { title: 'Agent Pool', dataIndex: 'pool_name', key: 'pool_name', render: (text, record) => ( {text || 'Unknown'} {record.pool_environment || 'Unknown environment'} ), width: 180, }, { title: 'Platform', key: 'platform', render: (_, record) => ( {record.platform} {record.architecture} ), width: 150, }, { title: 'HAProxy Status', key: 'haproxy_status', render: (_, record) => ( {record.haproxy_status || 'unknown'} ), width: 120, }, { title: 'Status', key: 'status', render: (_, record) => getStatusBadge(record.health, record.status, record.last_seen), width: 120, }, { title: 'Actions', key: 'actions', render: (_, record) => { const isLoading = configLoading && selectedAgent?.id === record.id; if (isLoading) { return (
Waiting for agent...
); } return ( ); }, width: 220, fixed: 'right', }, ]; return (
<Space> <FileTextOutlined /> Configuration Management </Space> View and download active haproxy.cfg files from agents. Configuration files are retrieved directly from agents in real-time. {/* Info Alert */}
  • Click "View" or "Download" to request the configuration file from an agent
  • The agent will retrieve its current haproxy.cfg file on the next heartbeat
  • The configuration will be displayed or downloaded once available
  • Agents must be online to process configuration requests
} type="info" showIcon style={{ marginTop: '16px' }} />
Agents - {selectedCluster?.name || 'No cluster selected'}} extra={
handleSearch(e.target.value)} style={{ width: '100%', height: 32, paddingLeft: 8, paddingRight: searchText ? 32 : 8, border: `1px solid ${token.colorBorder}`, borderRadius: 6, fontSize: 14, outline: 'none', boxShadow: 'none', backgroundColor: token.colorBgContainer, transition: 'border-color 0.3s ease' }} onFocus={(e) => { e.target.style.borderColor = '#1890ff'; e.target.style.outline = 'none'; e.target.style.boxShadow = 'none'; }} onBlur={(e) => { e.target.style.borderColor = token.colorBorder; }} /> {searchText && ( handleSearch('')} style={{ position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)', cursor: 'pointer', color: '#bfbfbf', fontSize: 14 }} /> )}
} > {!selectedCluster ? ( // Phase J audit fix #6 — show a neutral "Loading…" state // while the ClusterContext is still fetching, otherwise // the operator sees "No Cluster Selected" during the // legitimate post-deploy fetch window. clustersLoading ? ( ) : ( ) ) : agents.length === 0 && !loading ? (
No Agents Found No agents are registered in this cluster yet.
) : ( `${range[0]}-${range[1]} of ${total} agents`, }} scroll={{ x: 1000 }} size="middle" /> )} {/* Configuration Modal */} Configuration - {selectedAgent?.name} } open={configModalVisible} onCancel={() => { setConfigModalVisible(false); setConfigContent(''); }} width="90%" footer={[ , ]} >
{selectedAgent?.config_path || 'haproxy.cfg'}