// 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...
Cancel
);
}
return (
}
onClick={() => viewConfig(record)}
disabled={record.status !== 'online'}
>
View
}
onClick={() => downloadConfig(record)}
disabled={record.status !== 'online'}
>
Download
);
},
width: 220,
fixed: 'right',
},
];
return (
Configuration Management
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
}}
/>
)}
}
onClick={fetchAgents}
loading={loading}
type="primary"
ghost
>
Refresh
}
>
{!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={[
}
onClick={() => {
const element = document.createElement('a');
const file = new Blob([configContent], { type: 'text/plain' });
element.href = URL.createObjectURL(file);
element.download = `${selectedAgent?.name}-haproxy.cfg`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
message.success('Configuration downloaded');
}}
>
Download
,
setConfigModalVisible(false)}>
Close
]}
>
{selectedAgent?.config_path || 'haproxy.cfg'}
);
};
export default Configuration;