import React, { useState, useEffect } from 'react'; import { Card, Table, Button, Modal, Form, Input, InputNumber, Select, Switch, Space, message, Popconfirm, Tag, Tooltip, Row, Col, Typography, Divider, Checkbox, Collapse, Alert, InputNumber as AntInputNumber, Spin, Progress } from 'antd'; import { getAgentSyncColor, getConfigStatusColor, getEntityStatusColor, getSSLExpiryInfo } from '../utils/colors'; import EntitySyncStatus from './EntitySyncStatus'; import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, GlobalOutlined, SettingOutlined, LockOutlined, SafetyCertificateOutlined, WarningOutlined, SearchOutlined, HistoryOutlined, PlayCircleOutlined, LoadingOutlined } from '@ant-design/icons'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; import { useCluster } from '../contexts/ClusterContext'; import { VersionHistory } from './VersionHistory'; // Error Boundary Component class FrontendErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, errorInfo) { console.error('Frontend Management Error:', error, errorInfo); } render() { if (this.state.hasError) { return ( window.location.reload()}> Refresh Page } /> ); } return this.props.children; } } const { Option } = Select; const { Title, Text } = Typography; const { Panel } = Collapse; const { TextArea } = Input; const FrontendManagement = () => { const { selectedCluster } = useCluster(); const navigate = useNavigate(); const [frontends, setFrontends] = useState([]); const [backends, setBackends] = useState([]); const [sslCertificates, setSslCertificates] = useState([]); // Port conflict validation helper const validatePortConflict = (port, sslPort, currentFrontendId = null) => { const conflicts = []; frontends.forEach(frontend => { // Skip current frontend when editing if (currentFrontendId && frontend.id === currentFrontendId) return; // Check HTTP port conflicts if (frontend.bind_port === port) { conflicts.push(`Port ${port} is already used by frontend "${frontend.name}" (HTTP)`); } // Check HTTPS port conflicts (if frontend has SSL enabled) if (frontend.ssl_enabled && frontend.ssl_port === port) { conflicts.push(`Port ${port} is already used by frontend "${frontend.name}" (HTTPS)`); } // Check if our SSL port conflicts with existing HTTP ports if (sslPort && frontend.bind_port === sslPort) { conflicts.push(`HTTPS port ${sslPort} is already used by frontend "${frontend.name}" (HTTP)`); } // Check if our SSL port conflicts with existing HTTPS ports if (sslPort && frontend.ssl_enabled && frontend.ssl_port === sslPort) { conflicts.push(`HTTPS port ${sslPort} is already used by frontend "${frontend.name}" (HTTPS)`); } }); return conflicts; }; const [loading, setLoading] = useState(false); const [sslLoading, setSslLoading] = useState(false); const [modalVisible, setModalVisible] = useState(false); const [refreshKey, setRefreshKey] = useState(0); const [editingFrontend, setEditingFrontend] = useState(null); const [searchText, setSearchText] = useState(''); const [filteredFrontends, setFilteredFrontends] = useState([]); const [showPending, setShowPending] = useState(true); // Default TRUE: users must see their changes const [showRejected, setShowRejected] = useState(true); // Default TRUE: users must see rejected items // Persist toggle states across navigation useEffect(() => { const sp = localStorage.getItem('frontend:showPending'); const sr = localStorage.getItem('frontend:showRejected'); if (sp !== null) setShowPending(sp === 'true'); if (sr !== null) setShowRejected(sr === 'true'); }, []); const onToggleShowPending = (checked) => { setShowPending(checked); localStorage.setItem('frontend:showPending', String(checked)); }; const onToggleShowRejected = (checked) => { setShowRejected(checked); localStorage.setItem('frontend:showRejected', String(checked)); }; const [submitting, setSubmitting] = useState(false); const [applyLoading, setApplyLoading] = useState(false); const [pendingChanges, setPendingChanges] = useState(false); const [versionModalVisible, setVersionModalVisible] = useState(false); const [selectedEntityForVersion, setSelectedEntityForVersion] = useState(null); const [form] = Form.useForm(); // SSL visibility control - fields start visible for proper form registration const updateSSLVisibility = (sslEnabled) => { const sslFields = document.querySelectorAll('.ssl-fields'); sslFields.forEach(field => { field.style.display = sslEnabled ? 'block' : 'none'; }); }; // Status filter function - defined before fetchFrontends to avoid hoisting issues const applyStatusFilters = (items) => { return (items || []).filter(item => { const isPending = !!item.has_pending_config; const status = item.config_status || (isPending ? 'PENDING' : 'APPLIED'); if (!showPending && isPending) return false; if (!showRejected && status === 'REJECTED') return false; return true; }); }; useEffect(() => { // CRITICAL FIX: Clear state when cluster changes to prevent showing other cluster's data // Race condition: Old cluster data remains visible while new cluster data is fetching if (selectedCluster) { setFrontends([]); setFilteredFrontends([]); setBackends([]); setSslCertificates([]); } fetchFrontends(); fetchBackends(); fetchSSLCertificates(); checkPendingChanges(); }, [selectedCluster]); const fetchFrontends = async () => { // CRITICAL FIX: Don't fetch if no cluster selected to prevent race condition // Same race condition as BackendServers - prevents all frontends from appearing if (!selectedCluster) { setFrontends([]); setFilteredFrontends([]); return; } setLoading(true); try { const params = { cluster_id: selectedCluster.id }; const response = await axios.get('/api/frontends', { params, headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' } }); // šŸ” DEBUG: Log frontend data to check SSL fields console.log('šŸ” FRONTEND FETCH DEBUG: Response data:', response.data); if (response.data.frontends && response.data.frontends.length > 0) { response.data.frontends.forEach(frontend => { if (frontend.ssl_enabled) { console.log(`šŸ” SSL FRONTEND DEBUG: ${frontend.name}`, { ssl_enabled: frontend.ssl_enabled, ssl_certificate_id: frontend.ssl_certificate_id, ssl_port: frontend.ssl_port }); } }); } const fetchedFrontends = response.data.frontends; setFrontends(fetchedFrontends); // CRITICAL FIX: Apply status filters after fetching to maintain filter state // This prevents frontends from disappearing when updated (e.g., APPLIED → PENDING) setFilteredFrontends(applyStatusFilters(fetchedFrontends)); } catch (error) { message.error('Failed to fetch frontends: ' + error.message); } finally { setLoading(false); } }; // Search filter function const handleSearch = (value) => { setSearchText(value); if (!value) { setFilteredFrontends(applyStatusFilters(frontends)); } else { const filtered = frontends.filter(frontend => frontend.name.toLowerCase().includes(value.toLowerCase()) || frontend.bind_address.toLowerCase().includes(value.toLowerCase()) || frontend.default_backend?.toLowerCase().includes(value.toLowerCase()) || frontend.mode.toLowerCase().includes(value.toLowerCase()) ); setFilteredFrontends(applyStatusFilters(filtered)); } }; // Version history modal handlers const handleShowVersionHistory = (record) => { setSelectedEntityForVersion({ entityType: 'frontend', entityId: record.id }); setVersionModalVisible(true); }; const handleVersionModalCancel = () => { setVersionModalVisible(false); setSelectedEntityForVersion(null); }; const handleRestoreSuccess = (restoreData) => { // Refresh pending changes status after successful restore checkPendingChanges(); // Refresh frontends list to show updated data fetchFrontends(); message.info(
Configuration restored as PENDING
Use "Apply Changes" button to activate the restored configuration
, 4 ); }; // Update filtered data when frontends change useEffect(() => { if (searchText) { handleSearch(searchText); } else { setFilteredFrontends(applyStatusFilters(frontends)); } }, [frontends, showPending, showRejected]); const fetchBackends = async () => { // CRITICAL FIX: Guard clause to prevent race condition if (!selectedCluster) { setBackends([]); return; } try { const params = { cluster_id: selectedCluster.id }; const response = await axios.get('/api/backends', { params, headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' } }); setBackends(response.data.backends); } catch (error) { console.error('Failed to fetch backends:', error); } }; const fetchSSLCertificates = async () => { if (!selectedCluster) return; setSslLoading(true); try { console.log('šŸ” SSL FETCH DEBUG: Fetching FRONTEND certificates for cluster:', selectedCluster.id); const token = localStorage.getItem('token'); console.log('šŸ” SSL FETCH DEBUG: Token exists:', token ? 'Yes' : 'No'); // CRITICAL: Only fetch frontend SSL certificates (usage_type=frontend) const response = await axios.get(`/api/ssl/certificates?cluster_id=${selectedCluster.id}&usage_type=frontend`, { headers: { 'Authorization': `Bearer ${token}`, 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' } }); console.log('šŸ” SSL FETCH DEBUG: Response received:', response.data); // Handle different response formats and ensure we have the new fields const certs = response.data.certificates || response.data || []; console.log('šŸ” SSL FETCH DEBUG: Certificates processed:', certs.length, certs); console.log('šŸ” SSL FETCH DEBUG: Certificate details:', certs.map(cert => ({ id: cert.id, name: cert.name, domain: cert.domain || cert.primary_domain, ssl_type: cert.ssl_type }))); setSslCertificates(certs); } catch (error) { console.error('šŸ” SSL FETCH DEBUG: Failed to fetch SSL certificates:', error); console.error('šŸ” SSL FETCH DEBUG: Error response:', error.response?.data); console.error('šŸ” SSL FETCH DEBUG: Error status:', error.response?.status); setSslCertificates([]); } finally { setSslLoading(false); } }; const fetchEntityAgentSync = async (entityType, entityId) => { if (!selectedCluster) return null; try { const token = localStorage.getItem('token'); const response = await axios.get(`/api/clusters/${selectedCluster.id}/entity-sync/${entityType}/${entityId}`, { headers: { Authorization: `Bearer ${token}` } }); return response.data; } catch (error) { console.error(`Failed to fetch entity sync for ${entityType}/${entityId}:`, error); return null; } }; // Check for pending configuration changes const checkPendingChanges = async () => { if (!selectedCluster) return; try { console.log('šŸŽÆ APPLY DEBUG: Checking pending changes for cluster:', selectedCluster.id); const response = await axios.get(`/api/clusters/${selectedCluster.id}/config-versions`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); const versions = response.data.versions || []; const hasPending = versions.some(version => version.status === 'PENDING'); console.log('šŸŽÆ APPLY DEBUG: Config versions:', versions.length); console.log('šŸŽÆ APPLY DEBUG: Pending versions:', versions.filter(v => v.status === 'PENDING').length); console.log('šŸŽÆ APPLY DEBUG: Has pending changes:', hasPending); setPendingChanges(hasPending); } catch (error) { console.error('šŸŽÆ APPLY DEBUG: Failed to check pending changes:', error); } }; // Apply pending configuration changes const handleApplyChanges = async () => { if (!selectedCluster) return; setApplyLoading(true); try { const token = localStorage.getItem('token'); if (!token || token === 'null' || token.trim() === '') { message.error('Authentication required. Please login again.'); return; } const response = await axios.post( `/api/clusters/${selectedCluster.id}/apply-changes`, {}, { headers: { 'Authorization': `Bearer ${token}` } } ); message.success(response.data.message); // Refresh pending changes status from server await checkPendingChanges(); // Also refresh the frontends list to update Config Status column console.log('šŸŽÆ APPLY DEBUG: Refreshing frontends after apply...'); await fetchFrontends(); // Show sync results if available if (response.data.sync_results && response.data.sync_results.length > 0) { const agentResults = response.data.sync_results.filter(r => r.success); if (agentResults.length > 0) { message.info(`Configuration applied to ${agentResults.length} agent(s). HAProxy reloaded.`); } } } catch (error) { console.error('Apply changes failed:', error); if (error.response?.status === 401) { message.error('Authentication failed. Please login again.'); } else if (error.response?.data?.error === 'COMPREHENSIVE_VALIDATION_FAILED') { // Handle comprehensive validation errors from API const validationErrors = error.response.data.validation_errors || []; Modal.error({ title: '🚫 Configuration Validation Failed', content: (

Multiple configuration issues detected!

{error.response.data.message}

{validationErrors.map((error, index) => (

{error.type === 'BACKENDS_WITHOUT_SERVERS' && 'šŸ”ø Backends Missing Servers'} {error.type === 'UNUSED_BACKENDS' && 'šŸ”ø Unused Backends'} {error.type === 'FRONTENDS_WITH_MISSING_BACKENDS' && 'šŸ”ø Frontend → Backend Mismatch'} {error.type === 'NO_FRONTENDS' && 'šŸ”ø No Frontends Defined'}

{error.message}

Items:

    {error.items.map((item, idx) => (
  • {item}
  • ))}

šŸ’” Solution: {error.solution}

))}

šŸŽÆ Quick Actions:

), width: 800, okText: 'I Understand', okType: 'primary' }); } else if (error.response?.data?.error === 'BACKEND_VALIDATION_FAILED') { // Legacy: Handle old backend validation error from API const backendNames = error.response.data.details?.backends_without_servers || []; Modal.error({ title: '🚫 Configuration Apply Failed', content: (

Backend validation failed!

{error.response.data.message}


Backends without servers:

āœ… Solution: Go to Backend Management and add servers to these backends.


), width: 600, okText: 'I Understand', okType: 'default' }); } else { message.error(`Failed to apply changes: ${error.response?.data?.message || error.response?.data?.detail || error.message}`); } } finally { setApplyLoading(false); } }; const handleAdd = () => { setEditingFrontend(null); form.resetFields(); form.setFieldsValue({ bind_address: '*', mode: 'http', ssl_enabled: false, ssl_certificate_ids: [] }); // Update SSL field visibility for new frontend setTimeout(() => { updateSSLVisibility(false); }, 100); setModalVisible(true); }; const handleEdit = (frontend) => { setEditingFrontend(frontend); console.log('šŸ” FRONTEND EDIT DEBUG: Raw frontend data:', frontend); console.log('šŸ” FRONTEND EDIT DEBUG: SSL fields:', { ssl_enabled: frontend.ssl_enabled, ssl_certificate_ids: frontend.ssl_certificate_ids, ssl_certificate_id: frontend.ssl_certificate_id }); console.log('šŸ” FRONTEND EDIT DEBUG: TCP Request Rules:', frontend.tcp_request_rules); console.log('šŸ” FRONTEND EDIT DEBUG: Request Headers:', frontend.request_headers); console.log('šŸ” FRONTEND EDIT DEBUG: Options:', frontend.options); // ENTERPRISE DUAL-MODE: Support both old and new SSL format let sslCertIds = []; if (frontend.ssl_certificate_ids && Array.isArray(frontend.ssl_certificate_ids)) { // NEW: Multiple SSL certificates sslCertIds = frontend.ssl_certificate_ids; } else if (frontend.ssl_certificate_id) { // OLD: Single SSL certificate - convert to array sslCertIds = [frontend.ssl_certificate_id]; } // Format ACL rules for textarea (array to multi-line string) let formattedAclRules = frontend.acl_rules; if (Array.isArray(frontend.acl_rules)) { formattedAclRules = frontend.acl_rules.join('\n'); } // Format redirect rules for textarea (array to multi-line string) let formattedRedirectRules = frontend.redirect_rules; if (Array.isArray(frontend.redirect_rules)) { formattedRedirectRules = frontend.redirect_rules.join('\n'); } // Format use_backend rules for textarea (array to multi-line string) let formattedUseBackendRules = frontend.use_backend_rules; if (Array.isArray(frontend.use_backend_rules)) { formattedUseBackendRules = frontend.use_backend_rules.join('\n'); } form.setFieldsValue({ ...frontend, ssl_enabled: frontend.ssl_enabled || false, ssl_certificate_ids: sslCertIds, acl_rules: formattedAclRules, redirect_rules: formattedRedirectRules, use_backend_rules: formattedUseBackendRules, // Explicitly set options field to handle null/undefined case (NEW field) options: frontend.options || '', // BUGFIX: Explicitly set tcp_request_rules field to handle null/undefined case tcp_request_rules: frontend.tcp_request_rules || '' }); // Update SSL field visibility after setting values setTimeout(() => { updateSSLVisibility(frontend.ssl_enabled || false); }, 100); setModalVisible(true); }; const handleDelete = async (frontendId) => { if (!selectedCluster) { message.warning('Please select a HAProxy cluster first'); return; } // Find the frontend to get details for confirmation const frontend = frontends.find(f => f.id === frontendId); if (!frontend) { message.error('Frontend not found'); return; } // Check if frontend uses a backend let confirmMessage = `Are you sure you want to delete frontend "${frontend.name}"?`; let warningDetails = []; if (frontend.default_backend) { // Check if the backend still exists const backendExists = backends.some(b => b.name === frontend.default_backend); if (backendExists) { // Show error - frontend uses an active backend Modal.error({ title: 'Cannot Delete Frontend', content: (

Cannot delete frontend "{frontend.name}"

Reason: This frontend uses backend "{frontend.default_backend}"

Solution: Please delete the backend first, then the frontend will be automatically updated.

), okText: 'Understood' }); return; } } // Check WAF rules (just for info, they will be auto-removed) // Note: We don't have easy access to WAF rules here, so we'll rely on backend handling // Show confirmation modal Modal.confirm({ title: 'Delete Frontend', content: (

{confirmMessage}

{frontend.default_backend && (

Note: Any associated WAF rules will also be removed.

)}
), okText: 'Delete', okType: 'danger', cancelText: 'Cancel', onOk: async () => { try { const response = await axios.delete(`/api/frontends/${frontendId}`, { data: { cluster_id: selectedCluster.id } }); // Show frontend deletion success with details const frontendMessage = response.data.message || 'Frontend deleted successfully'; if (response.data.sync_results) { const syncResults = response.data.sync_results; const successCount = syncResults.filter(r => r.success).length; const totalNodes = syncResults.length; if (successCount === totalNodes) { message.success(
{frontendMessage}
āœ… Configuration updated on {successCount} cluster node(s)
šŸ“… Completed: {new Date().toLocaleString()}
, 8 ); } else { message.warning(
{frontendMessage}
āš ļø {successCount}/{totalNodes} cluster node(s) updated successfully
šŸ“… Completed: {new Date().toLocaleString()}
, 10 ); } } else { message.success(frontendMessage, 6); } fetchFrontends(); checkPendingChanges(); } catch (error) { const errorMsg = error.response?.data?.detail || error.message; message.error(
Failed to delete frontend
{errorMsg}
, 8 ); } } }); }; const handleSubmit = async (values) => { if (!selectedCluster) { message.warning('Please select a HAProxy cluster first'); return; } setSubmitting(true); try { // DEBUG: Log form values to see what's being sent console.log('šŸŽÆ FORM VALUES:', values); console.log('šŸŽÆ SSL CERTIFICATE DEBUG:', { ssl_enabled: values.ssl_enabled, ssl_certificate_id: values.ssl_certificate_id, ssl_port: values.ssl_port, sslCertificatesCount: sslCertificates.length }); // DEBUG: Log editing context if (editingFrontend) { console.log('šŸŽÆ EDITING FRONTEND:', { id: editingFrontend.id, name: editingFrontend.name, original_ssl_enabled: editingFrontend.ssl_enabled, original_ssl_certificate_id: editingFrontend.ssl_certificate_id, original_ssl_port: editingFrontend.ssl_port }); } const requestData = { ...values, cluster_id: selectedCluster.id }; // DEBUG: Log request data to see final payload console.log('šŸŽÆ REQUEST DATA:', requestData); let response; if (editingFrontend) { response = await axios.put(`/api/frontends/${editingFrontend.id}`, requestData); // Show agent sync results if (response.data.sync_results) { const syncResults = response.data.sync_results; const agentCount = syncResults.length; const successfulAgents = syncResults.filter(r => r.success).length; message.success(
Frontend updated successfully!
šŸ“ Changes created as PENDING
šŸ”„ Click "Apply Changes" to activate configuration
šŸ“… Updated: {new Date().toLocaleString()}
, 6 ); } else { message.success('Frontend updated successfully'); } } else { response = await axios.post('/api/frontends', requestData); // Show agent sync results if (response.data.sync_results) { const syncResults = response.data.sync_results; const agentCount = syncResults.length; const successfulAgents = syncResults.filter(r => r.success).length; message.success(
Frontend created successfully!
šŸ“ Changes created as PENDING
šŸ”„ Click "Apply Changes" to activate configuration
šŸ“… Created: {new Date().toLocaleString()}
, 6 ); } else { message.success('Frontend created successfully'); } } setModalVisible(false); fetchFrontends(); fetchSSLCertificates(); // Refresh SSL certificates after frontend update checkPendingChanges(); } catch (error) { const errorMsg = error.response?.data?.detail || error.message; message.error(
Failed to save frontend
{errorMsg}
, 8 ); } finally { setSubmitting(false); } }; const columns = [ { title: 'Name', dataIndex: 'name', key: 'name', render: (text, record) => ( {text} {!record.is_active && Inactive} ), }, { title: 'Sync Status', key: 'sync_status', render: (_, record) => ( ), }, { title: 'Bind Address', dataIndex: 'bind_address', key: 'bind_address', render: (text, record) => `${text}:${record.bind_port}`, }, { title: 'Mode', dataIndex: 'mode', key: 'mode', render: (mode) => ( {mode.toUpperCase()} ), }, { title: 'Default Backend', dataIndex: 'default_backend', key: 'default_backend', render: (backend) => backend ? ( {backend} ) : Not Set, }, { title: 'SSL/TLS', key: 'ssl_info', width: 200, render: (_, record) => { if (!record.ssl_enabled) { return No SSL; } // ENTERPRISE DUAL-MODE: Support both ssl_certificate_ids (new) and ssl_certificate_id (old) let certIds = []; if (record.ssl_certificate_ids && Array.isArray(record.ssl_certificate_ids)) { certIds = record.ssl_certificate_ids; } else if (record.ssl_certificate_id) { certIds = [record.ssl_certificate_id]; } if (sslLoading) { return (
} size="small"> Loading SSL
Fetching certificate info...
); } if (certIds.length === 0) { return (
} size="small"> SSL Enabled
No certificates assigned
); } // Find all SSL certificates const sslCerts = certIds.map(id => sslCertificates.find(cert => cert.id === id)).filter(Boolean); if (sslCerts.length === 0) { return (
} size="small"> SSL Error
Certificate(s) not found
); } // Single certificate - show full details if (sslCerts.length === 1) { const sslCert = sslCerts[0]; const expiryInfo = getSSLExpiryInfo(sslCert.expiry_date); const domain = sslCert.domain || sslCert.primary_domain || sslCert.name; return (
} size="small" style={{ marginBottom: '6px', fontSize: '11px' }} > {domain}
{domain}
Status: {expiryInfo.status}
{sslCert.expiry_date && (
Expires: {new Date(sslCert.expiry_date).toLocaleDateString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric' })}
)}
} placement="top" >
{expiryInfo.status}
); } // Multiple certificates - show compact list const allValid = sslCerts.every(cert => { const expiry = getSSLExpiryInfo(cert.expiry_date); return expiry.tagColor === 'green'; }); return (
} size="small" style={{ marginBottom: '4px' }} > {sslCerts.length} Certificates
SSL Certificates ({sslCerts.length}):
{sslCerts.map(cert => { const expiryInfo = getSSLExpiryInfo(cert.expiry_date); return (
• {cert.domain || cert.name} - {expiryInfo.status}
); })}
} placement="top" >
{sslCerts.map(cert => (cert.domain || cert.name)).join(', ').substring(0, 40)}...
); }, }, { title: 'Status', dataIndex: 'is_active', key: 'status', render: (isActive) => ( {isActive ? 'Active' : 'Inactive'} ), }, { title: 'Created', dataIndex: 'created_at', key: 'created_at', render: (date) => date ? new Date(date).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '-', }, { title: 'Last Update', dataIndex: 'updated_at', key: 'updated_at', render: (date) => date ? new Date(date).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '-', }, { title: 'Config Status', key: 'config_status', render: (_, record) => { const status = record.config_status || (record.has_pending_config ? 'PENDING' : 'APPLIED'); const color = getConfigStatusColor(status); return {status}; }, }, { title: 'Actions', key: 'actions', render: (_, record) => ( {(record.has_pending_config || false) && ( )} ); } return (

Frontend Management

Pending Rejected
handleSearch(e.target.value)} style={{ width: '100%', height: 32, paddingLeft: 8, paddingRight: 8, border: '1px solid #d9d9d9', borderRadius: 6, fontSize: 14, outline: 'none', boxShadow: 'none', backgroundColor: '#fff', 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 = '#d9d9d9'; }} onMouseOver={(e) => { if (e.target !== document.activeElement) { e.target.style.borderColor = '#40a9ff'; } }} onMouseOut={(e) => { if (e.target !== document.activeElement) { e.target.style.borderColor = '#d9d9d9'; } }} />
{pendingChanges && ( )}
`${range[0]}-${range[1]} of ${total} frontends`, }} /> setModalVisible(false)} footer={null} width={900} >
{/* Basic Configuration */}
({ validator(_, value) { if (!value) return Promise.resolve(); // Only check HTTP port conflicts, not SSL port conflicts for HTTP field const conflicts = validatePortConflict(value, null, editingFrontend?.id); if (conflicts.length > 0) { return Promise.reject(new Error(`Port conflict detected:\n• ${conflicts.join('\n• ')}`)); } return Promise.resolve(); }, }), ]} > {/* SSL Configuration */} { updateSSLVisibility(checked); }} /> {/* SSL Fields - Always registered in form, visibility controlled via state */} {/* Advanced Options */} {/* ACL Rules */}