mirror of
https://github.com/taylanbakircioglu/haproxy-openmanager.git
synced 2026-10-03 12:10:44 +00:00
c2766eadbd
- 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
227 lines
7.9 KiB
JavaScript
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;
|