import React, { useState, useEffect } from 'react';
import {
Table, Button, Space, Modal, Form, Input, Select, Tag, message, Switch,
Tooltip, Popconfirm, Row, Col, Divider, Typography, Badge, Card, Alert, theme
} from 'antd';
import {
PlusOutlined, EditOutlined, DeleteOutlined,
LoadingOutlined, CloudServerOutlined, DownloadOutlined, SettingOutlined,
SafetyCertificateOutlined, UsergroupAddOutlined, SearchOutlined, WarningOutlined
} from '@ant-design/icons';
import { useCluster } from '../contexts/ClusterContext';
import { extractApiError } from '../utils/apiError';
const { Option } = Select;
const { TextArea } = Input;
const { Title, Text } = Typography;
const ClusterManagement = () => {
const {
clusters,
loading,
addCluster,
updateCluster,
deleteCluster,
fetchClusters
} = useCluster();
const [modalVisible, setModalVisible] = useState(false);
const [editingCluster, setEditingCluster] = useState(null);
const [form] = Form.useForm();
const [searchText, setSearchText] = useState('');
const [filteredClusters, setFilteredClusters] = useState([]);
// Agent Management Integration
const [agentPools, setAgentPools] = useState([]);
const [loadingPools, setLoadingPools] = useState(false);
// Path Discovery
const [pathDiscoveryVisible, setPathDiscoveryVisible] = useState(false);
// Agent HAProxy versions per cluster
const [clusterAgents, setClusterAgents] = useState({});
const [loadingAgents, setLoadingAgents] = useState(false);
const showPathDiscovery = () => {
setPathDiscoveryVisible(true);
};
// Fetch agents for all clusters to get haproxy_version info
const fetchAgentsForClusters = async (clusterList) => {
if (!clusterList || clusterList.length === 0) return;
setLoadingAgents(true);
const agentsByCluster = {};
try {
// Get unique pool_ids from clusters
const poolIds = [...new Set(clusterList.filter(c => c.pool_id).map(c => c.pool_id))];
// Fetch agents for each pool
for (const poolId of poolIds) {
try {
const response = await fetch(`/api/agents?pool_id=${poolId}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('authToken') || ''}`,
},
});
if (response.ok) {
const data = await response.json();
const agents = data.agents || [];
// Map agents to clusters by pool_id
clusterList.filter(c => c.pool_id === poolId).forEach(cluster => {
agentsByCluster[cluster.id] = agents.map(a => ({
name: a.name,
haproxy_version: a.haproxy_version,
status: a.status,
keepalive_state: a.keepalive_state,
keepalive_ip: a.keepalive_ip
}));
});
}
} catch (err) {
console.error(`Error fetching agents for pool ${poolId}:`, err);
}
}
setClusterAgents(agentsByCluster);
} catch (error) {
console.error('Error fetching agents for clusters:', error);
} finally {
setLoadingAgents(false);
}
};
useEffect(() => {
fetchClusters();
fetchAgentPools();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
setFilteredClusters(clusters);
// Fetch agents for all clusters to display HAProxy versions
if (clusters && clusters.length > 0) {
fetchAgentsForClusters(clusters);
}
}, [clusters]);
const { token } = theme.useToken();
// Fetch Agent Pools for Agent connection type
const fetchAgentPools = async () => {
setLoadingPools(true);
try {
const response = await fetch('/api/haproxy-cluster-pools', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('authToken') || ''}`,
},
});
if (response.ok) {
const data = await response.json();
setAgentPools(data.pools || []); // Extract pools array from response
} else {
console.error('Failed to fetch agent pools:', response.status, response.statusText);
setAgentPools([]);
}
} catch (error) {
console.error('Error fetching agent pools:', error);
setAgentPools([]);
} finally {
setLoadingPools(false);
}
};
const handleAddCluster = () => {
setEditingCluster(null);
form.resetFields();
form.setFieldsValue({
connection_type: 'agent'
// ✅ FIXED: No default values - let user enter their own paths
});
setModalVisible(true);
};
const handleEditCluster = (cluster) => {
setEditingCluster(cluster);
// 🔍 DEBUG: Log cluster data to see what we're getting from backend
console.log('🔍 CLUSTER EDIT DEBUG - Raw cluster data:', cluster);
console.log('🔍 CLUSTER EDIT DEBUG - haproxy_bin_path:', cluster.haproxy_bin_path);
const formValues = {
name: cluster.name,
description: cluster.description,
connection_type: cluster.connection_type,
stats_socket_path: cluster.stats_socket_path || '/run/haproxy/admin.sock',
haproxy_config_path: cluster.haproxy_config_path || '/etc/haproxy/haproxy.cfg',
haproxy_bin_path: cluster.haproxy_bin_path,
keepalived_config_path: cluster.keepalived_config_path || '/etc/keepalived/keepalived.conf',
agent_pool_id: cluster.pool_id || undefined,
haproxy_user: cluster.haproxy_user || '',
haproxy_group: cluster.haproxy_group || '',
acme_enabled: cluster.acme_enabled || false
};
console.log('🔍 CLUSTER EDIT DEBUG - Form values being set:', formValues);
form.setFieldsValue(formValues);
setModalVisible(true);
};
const handleDeleteCluster = async (clusterId) => {
try {
await deleteCluster(clusterId);
message.success('Cluster deleted successfully');
} catch (error) {
message.error('Failed to delete cluster: ' + (extractApiError(error, error.message)));
}
};
const handleModalCancel = () => {
setModalVisible(false);
setEditingCluster(null);
form.resetFields();
};
// Removed handleTestModalConnection
const handleModalOk = async () => {
try {
const values = await form.validateFields();
// Transform form values to match simplified backend API (agent-based only)
const transformedValues = {
name: values.name,
description: values.description,
connection_type: values.connection_type || 'agent',
stats_socket_path: values.stats_socket_path,
haproxy_config_path: values.haproxy_config_path,
haproxy_bin_path: values.haproxy_bin_path,
keepalived_config_path: values.keepalived_config_path,
pool_id: values.agent_pool_id || null,
haproxy_user: values.haproxy_user || null,
haproxy_group: values.haproxy_group || null,
acme_enabled: values.acme_enabled || false
};
// For new clusters, explicitly set is_active to true
// For existing clusters (edit), don't send is_active so backend keeps the current value
if (!editingCluster) {
transformedValues.is_active = true;
}
const response = await fetch(editingCluster ?
`/api/clusters/${editingCluster.id}` :
'/api/clusters', {
method: editingCluster ? 'PUT' : 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken') || localStorage.getItem('token') || ''}`,
},
body: JSON.stringify(transformedValues),
});
const result = await response.json();
if (response.ok) {
message.success(editingCluster ? 'Cluster updated successfully' : 'Cluster created successfully');
setModalVisible(false);
setEditingCluster(null);
form.resetFields();
await fetchClusters();
} else {
message.error(result.detail || result.message || 'An error occurred');
}
} catch (error) {
message.error('Error: ' + error.message);
}
};
const handleSearch = (value) => {
const filtered = clusters.filter(cluster => {
const nameMatch = cluster.name.toLowerCase().includes(value.toLowerCase());
const descMatch = cluster.description && cluster.description.toLowerCase().includes(value.toLowerCase());
const agents = clusterAgents[cluster.id] || [];
const keepaliveMatch = agents.some(a =>
a.keepalive_ip && a.keepalive_ip.toLowerCase().includes(value.toLowerCase())
);
return nameMatch || descMatch || keepaliveMatch;
});
setFilteredClusters(filtered);
setSearchText(value);
};
const getStatusBadge = (cluster) => {
const status = cluster.agent_status || cluster.connection_status || 'unknown';
const statusConfig = {
'healthy': { status: 'success', text: 'Healthy' },
'warning': { status: 'warning', text: 'Warning' },
'offline': { status: 'error', text: 'Offline' },
'no-agents': { status: 'default', text: 'No Agents' },
'connected': { status: 'success', text: 'Connected' },
'unknown': { status: 'default', text: 'Unknown' },
'error': { status: 'error', text: 'Error' }
};
const config = statusConfig[status] || statusConfig['unknown'];
return