Files
haproxy-openmanager/frontend/src/components/ClusterSelector.js
T
taylanbakircioglu c2766eadbd feat: dark mode support, UI improvements, and version bump to v1.2.0
- Full dark mode support across all pages with lightbulb toggle in header
- Theme preference persisted in localStorage across sessions
- Ant Design 5 token-based theming (40+ components updated)
- Recharts dark mode: axes, grids, tooltips adapt to theme
- Login page redesigned with product-consistent blue-gray palette
- Overscroll bounce background matches dark theme
- All Servers search with multi-field filtering
- Bulk Config Import UI streamlined with collapsible guidelines
- ConfigProvider moved above AppContent for correct token resolution

Made-with: Cursor
2026-04-03 01:16:05 +03:00

227 lines
7.9 KiB
JavaScript

import React from 'react';
import { Select, Badge, Tooltip, theme } from 'antd';
import { CloudServerOutlined, CheckCircleOutlined, ExclamationCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons';
import { useCluster } from '../contexts/ClusterContext';
const { Option } = Select;
const ClusterSelector = () => {
const { token } = theme.useToken();
const { clusters, selectedCluster, selectCluster, loading, agentHealthByPool } = useCluster();
const getAgentStatusIcon = (agentStatus, totalAgents) => {
if (totalAgents === 0) {
return <ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />;
}
switch (agentStatus) {
case 'healthy':
return <CheckCircleOutlined style={{ color: '#52c41a' }} />;
case 'warning':
return <ExclamationCircleOutlined style={{ color: '#faad14' }} />;
case 'offline':
return <CloseCircleOutlined style={{ color: '#ff4d4f' }} />;
default:
return <LoadingOutlined style={{ color: '#1890ff' }} />;
}
};
const getAgentStatusColor = (agentStatus, totalAgents) => {
if (totalAgents === 0) {
return '#ff4d4f'; // Red for no agents
}
switch (agentStatus) {
case 'healthy':
return '#52c41a'; // Green
case 'warning':
return '#faad14'; // Orange/Yellow
case 'offline':
return '#ff4d4f'; // Red
default:
return '#d9d9d9'; // Gray for unknown
}
};
const getAgentStatusText = (cluster) => {
const { total_agents, healthy_agents, warning_agents, offline_agents, agent_status } = cluster;
if (total_agents === 0) {
return 'No Agents';
}
const statusParts = [];
if (healthy_agents > 0) statusParts.push(`${healthy_agents} online`);
if (warning_agents > 0) statusParts.push(`${warning_agents} warning`);
if (offline_agents > 0) statusParts.push(`${offline_agents} offline`);
return statusParts.join(', ') || `${total_agents} ${agent_status}`;
};
if (loading) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<LoadingOutlined />
<span>Loading clusters...</span>
</div>
);
}
// Dynamic width based on selected cluster name length
// Respects viewport width to avoid overflow on smaller screens
const getSelectWidth = () => {
const vw = typeof window !== 'undefined' ? window.innerWidth : 1200;
// On small screens, use a compact width
if (vw < 992) return 200;
if (vw < 1200) return Math.min(280, vw * 0.25);
if (!selectedCluster) return 250;
const nameLen = selectedCluster.name?.length || 0;
const typeLen = (selectedCluster.connection_type || '').length;
// ~7.5px per char + padding for badge, icon, parentheses, gaps
const estimated = Math.ceil((nameLen + typeLen + 4) * 7.5) + 90;
return Math.max(250, Math.min(estimated, 500));
};
const isUnselected = !selectedCluster;
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 }}>
{isUnselected && (
<style>{`
.cluster-select-unselected .ant-select-selector {
border-color: #1890ff !important;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.15) !important;
}
`}</style>
)}
<CloudServerOutlined style={{ color: '#1890ff', flexShrink: 0 }} />
<Select
className={isUnselected ? 'cluster-select-unselected' : undefined}
value={selectedCluster?.id}
showSearch
style={{
width: getSelectWidth(),
minWidth: 180,
maxWidth: 500,
}}
placeholder="⬅ Select a Cluster"
onChange={(clusterId) => {
const cluster = clusters.find(c => c.id === clusterId);
if (cluster) {
selectCluster(cluster);
}
}}
filterOption={(input, option) => {
if (!input) return true;
const cluster = clusters.find(c => c.id === option?.value);
if (!cluster) return false;
const search = input.toLowerCase();
return (
(cluster.name || '').toLowerCase().includes(search) ||
(cluster.pool_name || '').toLowerCase().includes(search) ||
(cluster.connection_type || '').toLowerCase().includes(search) ||
(cluster.description || '').toLowerCase().includes(search)
);
}}
optionLabelProp="label"
size="middle"
>
{clusters.map(cluster => (
<Option
key={cluster.id}
value={cluster.id}
label={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', overflow: 'hidden' }}>
<Badge
dot
color={getAgentStatusColor(cluster.agent_status, cluster.total_agents)}
/>
<span style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
flexShrink: 1
}}>{cluster.name}</span>
<span style={{
fontSize: '11px',
color: token.colorTextSecondary,
whiteSpace: 'nowrap',
flexShrink: 0
}}>
({cluster.connection_type})
</span>
</div>
}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flex: 1 }}>
<Badge
dot
color={getAgentStatusColor(cluster.agent_status, cluster.total_agents)}
/>
<div>
<div style={{ fontWeight: 500 }}>
{cluster.name}
{cluster.is_default && (
<span style={{
marginLeft: '6px',
fontSize: '10px',
background: '#1890ff',
color: 'white',
padding: '1px 4px',
borderRadius: '2px'
}}>
DEFAULT
</span>
)}
</div>
<div style={{
fontSize: '12px',
color: token.colorTextSecondary,
marginTop: '2px'
}}>
Pool: {cluster.pool_name} ({cluster.connection_type})
</div>
<div style={{
fontSize: '11px',
color: cluster.total_agents === 0 ? '#ff4d4f' : '#52c41a',
marginTop: '1px',
fontWeight: 500
}}>
Agents: {getAgentStatusText(cluster)}
</div>
{cluster.description && (
<div style={{
fontSize: '11px',
color: '#999',
marginTop: '1px'
}}>
{cluster.description}
</div>
)}
</div>
</div>
<Tooltip title={`Agents: ${getAgentStatusText(cluster)}`}>
{getAgentStatusIcon(cluster.agent_status, cluster.total_agents)}
</Tooltip>
</div>
</Option>
))}
</Select>
{selectedCluster && (
<Tooltip title={`${selectedCluster.name} - Agents: ${getAgentStatusText(selectedCluster)}`}>
<Badge
dot
color={getAgentStatusColor(
selectedCluster.agent_status, // Use direct agent_status instead of cache
selectedCluster.total_agents
)}
/>
</Tooltip>
)}
</div>
);
};
export default ClusterSelector;