import React, { useState, useEffect, useContext } from 'react'; import { Card, Table, Button, Modal, Form, Input, Space, message, Popconfirm, Tag, Tooltip, Row, Col, Typography, Alert, Badge, Progress, Tabs, Select, Switch, Spin } from 'antd'; import { getAgentSyncColor, getConfigStatusColor, getEntityStatusColor } from '../utils/colors'; import EntitySyncStatus from './EntitySyncStatus'; import { PlusOutlined, DeleteOutlined, ReloadOutlined, LockOutlined, EyeOutlined, WarningOutlined, SafetyCertificateOutlined, SearchOutlined, PlayCircleOutlined, EditOutlined } from '@ant-design/icons'; import axios from 'axios'; import { useCluster } from '../contexts/ClusterContext'; import { useProgress } from '../contexts/ProgressContext'; import { formatEntityForSync } from '../utils/agentSync'; const { Title, Text } = Typography; const { TextArea } = Input; const { TabPane } = Tabs; const SSLManagement = () => { const { selectedCluster, clusters } = useCluster(); const [certificates, setCertificates] = useState([]); const [filteredCertificates, setFilteredCertificates] = useState([]); const [searchText, setSearchText] = useState(''); const [loading, setLoading] = useState(false); const [modalVisible, setModalVisible] = useState(false); const [viewModalVisible, setViewModalVisible] = useState(false); const [selectedCertificate, setSelectedCertificate] = useState(null); const [pendingChanges, setPendingChanges] = useState(false); const [form] = Form.useForm(); // Filter states with localStorage persistence const [showGlobal, setShowGlobal] = useState(() => { const saved = localStorage.getItem('ssl_filter_global'); return saved !== null ? JSON.parse(saved) : true; }); const [showClusterSpecific, setShowClusterSpecific] = useState(() => { const saved = localStorage.getItem('ssl_filter_cluster'); return saved !== null ? JSON.parse(saved) : true; }); // Filter toggle handlers const toggleGlobalFilter = () => { const newValue = !showGlobal; setShowGlobal(newValue); localStorage.setItem('ssl_filter_global', JSON.stringify(newValue)); }; const toggleClusterFilter = () => { const newValue = !showClusterSpecific; setShowClusterSpecific(newValue); localStorage.setItem('ssl_filter_cluster', JSON.stringify(newValue)); }; // Apply filters whenever certificates or filter states change useEffect(() => { let filtered = certificates; // Apply SSL type filters if (!showGlobal || !showClusterSpecific) { filtered = filtered.filter(cert => { if (cert.ssl_type === 'Global' && !showGlobal) return false; if (cert.ssl_type === 'Cluster-specific' && !showClusterSpecific) return false; return true; }); } // Apply search filter if (searchText) { filtered = filtered.filter(cert => cert.name.toLowerCase().includes(searchText.toLowerCase()) || cert.domain.toLowerCase().includes(searchText.toLowerCase()) || (cert.issuer && cert.issuer.toLowerCase().includes(searchText.toLowerCase())) ); } setFilteredCertificates(filtered); }, [certificates, showGlobal, showClusterSpecific, searchText]); useEffect(() => { if (selectedCluster) { fetchCertificates(); checkPendingChanges(); } else { // Clear certificates when no cluster is selected setCertificates([]); setFilteredCertificates([]); setPendingChanges(false); } }, [selectedCluster]); const fetchCertificates = async () => { if (!selectedCluster) return; setLoading(true); // Clear existing certificates immediately when fetching new cluster data setCertificates([]); setFilteredCertificates([]); try { const response = await axios.get(`/api/ssl/certificates?cluster_id=${selectedCluster.id}`, { headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' } }); // Handle different response formats const certs = response.data.certificates || response.data || []; console.log('🎯 SSL FETCH DEBUG: Response structure:', { 'response.data': Object.keys(response.data), 'certificates count': certs.length, 'sample cert': certs[0] ? { name: certs[0].name, has_pending_config: certs[0].has_pending_config, last_config_status: certs[0].last_config_status } : null }); setCertificates(certs); setFilteredCertificates(certs); } catch (error) { console.error('SSL certificates fetch error:', error); message.error('Failed to fetch SSL certificates: ' + error.message); setCertificates([]); setFilteredCertificates([]); } finally { setLoading(false); } }; // Search filter function const handleSearch = (value) => { setSearchText(value); if (!value) { setFilteredCertificates(certificates || []); } else { const filtered = certificates.filter(cert => cert.name.toLowerCase().includes(value.toLowerCase()) || cert.domain.toLowerCase().includes(value.toLowerCase()) ); setFilteredCertificates(filtered); } }; // Update filtered data when certificates change useEffect(() => { if (searchText) { handleSearch(searchText); } else { setFilteredCertificates(certificates); } }, [certificates, searchText]); // Check for pending SSL configuration changes const checkPendingChanges = async () => { if (!selectedCluster) return; try { console.log('🎯 SSL APPLY DEBUG: Checking pending changes for cluster:', selectedCluster.id); console.log('🔄 Frontend deployment check - v2.0'); const response = await axios.get(`/api/clusters/${selectedCluster.id}/config-versions`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); const versions = response.data.config_versions || response.data.versions || response.data || []; console.log('🎯 SSL APPLY DEBUG: Versions type:', typeof versions, 'Array?', Array.isArray(versions), 'Value:', versions); if (!Array.isArray(versions)) { console.error('🎯 SSL APPLY DEBUG: versions is not an array!', versions); setPendingChanges(false); return; } const pendingVersions = versions.filter(version => version.status === 'PENDING' && version.version_name.includes('ssl-') ); console.log('🎯 SSL APPLY DEBUG: Total versions:', versions.length); console.log('🎯 SSL APPLY DEBUG: SSL pending versions:', pendingVersions.length); console.log('🎯 SSL APPLY DEBUG: Pending SSL versions:', pendingVersions.map(v => v.version_name)); setPendingChanges(pendingVersions.length > 0); } catch (error) { console.error('🎯 SSL APPLY DEBUG: Failed to check pending changes:', error); setPendingChanges(false); } }; // Fetch entity agent sync status (consistent with other pages) const fetchEntityAgentSync = async (entityType, entityId) => { if (!selectedCluster) return null; try { const token = localStorage.getItem('token'); // SSL certificates use a special endpoint for agent sync let endpoint; if (entityType === 'ssl_certificates') { endpoint = `/api/clusters/${selectedCluster.id}/ssl_certificates/${entityId}/agent-sync`; } else { endpoint = `/api/clusters/${selectedCluster.id}/entity-sync/${entityType}/${entityId}`; } const response = await axios.get(endpoint, { headers: { Authorization: `Bearer ${token}` } }); return response.data; } catch (error) { console.error(`Failed to fetch entity sync for ${entityType}/${entityId}:`, error); return null; } }; const handleAdd = () => { form.resetFields(); // Set default values for new certificate form.setFieldsValue({ ssl_type: 'cluster' }); setSelectedCertificate(null); setModalVisible(true); }; const handleView = async (certificate) => { try { const response = await axios.get(`/api/ssl/certificates/${certificate.id}`); setSelectedCertificate(response.data); setViewModalVisible(true); } catch (error) { message.error('Failed to load certificate details: ' + error.message); } }; const handleEdit = async (certificate) => { try { const response = await axios.get(`/api/ssl/certificates/${certificate.id}`); const cert = response.data; // Pre-fill form with certificate data form.setFieldsValue({ name: cert.name, certificate_content: cert.certificate_content, private_key_content: cert.private_key_content, chain_content: cert.chain_content, ssl_type: cert.is_global ? 'global' : 'cluster', cluster_ids: cert.is_global ? null : cert.cluster_ids }); setSelectedCertificate(cert); setModalVisible(true); } catch (error) { message.error('Failed to load certificate for editing: ' + error.message); } }; const handleDelete = async (certificateId) => { try { const response = await axios.delete(`/api/ssl/certificates/${certificateId}`); // Handle cluster sync results const syncResults = response.data.sync_results || []; const totalNodes = syncResults.length; const successCount = syncResults.filter(result => result.success).length; if (syncResults.length > 0) { if (successCount === totalNodes) { message.success(
SSL certificate deleted successfully
✅ {successCount}/{totalNodes} nodes updated successfully
, 6 ); } else { message.warning(
SSL certificate deleted with warnings
✅ {successCount}/{totalNodes} nodes updated successfully
⚠️ Some nodes may need manual cleanup
, 8 ); } } else { message.success('SSL certificate deleted successfully'); } fetchCertificates(); } catch (error) { message.error('Failed to delete certificate: ' + error.response?.data?.detail); } }; const handleSubmit = async (values) => { // Check if editing existing certificate (define at function scope) const isEditing = selectedCertificate && selectedCertificate.id; try { // Prepare payload with new SSL model const payload = { name: values.name, certificate_content: values.certificate_content, private_key_content: values.private_key_content, chain_content: values.chain_content, is_global: values.ssl_type === 'global', cluster_ids: values.ssl_type === 'global' ? null : values.cluster_ids }; const response = isEditing ? await axios.put(`/api/ssl/certificates/${selectedCertificate.id}`, payload) : await axios.post('/api/ssl/certificates', payload); // Handle cluster sync results const syncResults = response.data.sync_results || []; const totalNodes = syncResults.length; const successCount = syncResults.filter(result => result.success).length; if (syncResults.length > 0) { if (successCount === totalNodes) { message.success(
SSL certificate {isEditing ? 'updated' : 'added'} successfully
✅ {successCount}/{totalNodes} nodes updated successfully
, 6 ); } else { message.warning(
SSL certificate {isEditing ? 'updated' : 'added'} with warnings
✅ {successCount}/{totalNodes} nodes updated successfully
⚠️ Some nodes may need manual certificate deployment
, 8 ); } } else { message.success(`SSL certificate ${isEditing ? 'updated' : 'added'} successfully`); } setModalVisible(false); setSelectedCertificate(null); fetchCertificates(); checkPendingChanges(); } catch (error) { console.error('SSL certificate operation failed:', error); // Handle specific error cases with user-friendly messages if (error.response?.status === 400) { const errorDetail = error.response?.data?.detail || ''; if (errorDetail.includes('already exists')) { // SSL name already exists error Modal.error({ title: 'SSL Certificate Name Already Exists', content: (

A certificate with the name "{form.getFieldValue('name')}" already exists.

Please choose one of the following options:

), okText: 'Got it', width: 500 }); } else if (errorDetail.includes('Invalid SSL certificate')) { // Invalid certificate content error Modal.error({ title: 'Invalid SSL Certificate', content: (

Certificate validation failed:

{errorDetail}

Please check your certificate content and try again.

), okText: 'Fix Certificate', width: 600 }); } else { // Other 400 errors message.error(`Certificate validation error: ${errorDetail}`); } } else if (error.response?.status === 401) { message.error('Authentication failed. Please login again.'); } else if (error.response?.status === 403) { message.error('You do not have permission to perform this action.'); } else { // Generic error message.error(`Failed to ${isEditing ? 'update' : 'add'} certificate: ${error.response?.data?.detail || error.message}`); } } }; const getExpiryStatus = (expiryDate, expiresSoon) => { if (!expiryDate) return { status: 'default', text: 'No expiry set' }; const days = Math.ceil((new Date(expiryDate) - new Date()) / (1000 * 60 * 60 * 24)); if (days < 0) return { status: 'error', text: 'Expired' }; if (days <= 7) return { status: 'error', text: `${days} days left` }; if (days <= 30) return { status: 'warning', text: `${days} days left` }; return { status: 'success', text: `${days} days left` }; }; const columns = [ { title: 'Certificate', dataIndex: 'name', key: 'name', render: (text, record) => (
{text}
{record.domain}
), }, { title: 'Type', dataIndex: 'ssl_type', key: 'ssl_type', render: (type, record) => { const isGlobal = type === 'Global'; return ( {isGlobal ? 'Global' : 'Cluster-specific'} ); }, }, { title: 'Sync Status', key: 'sync_status', render: (_, record) => ( ), }, { title: 'Expiry Status', dataIndex: 'expiry_date', key: 'expiry_status', render: (expiryDate, record) => { const status = getExpiryStatus(expiryDate, record.expires_soon); return ( ); }, }, { title: 'Expiry Date', dataIndex: 'expiry_date', key: 'expiry_date', render: (date) => date ? new Date(date).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '-', }, { 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: 'Config Status', key: 'config_status', render: (_, record) => { const status = record.last_config_status || 'APPLIED'; const color = getConfigStatusColor(status); return ( {status} ); }, }, { 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: 'Actions', key: 'actions', render: (_, record) => ( {(record.last_config_status === 'PENDING') && ( )} {expiringSoon.length > 0 && ( {expiringSoon.map(cert => (
{cert.name} ({cert.domain}) - {getExpiryStatus(cert.expiry_date, cert.expires_soon).text}
))} } type="warning" showIcon icon={} style={{ marginBottom: 16 }} /> )} {!selectedCluster && ( )}
{certificates.length}
Total Certificates
{certificates.filter(c => c.is_active).length}
Active
{expiringSoon.length}
Expiring Soon
{certificates.filter(c => c.expiry_date && new Date(c.expiry_date) < new Date()).length}
Expired
`${range[0]}-${range[1]} of ${total} certificates`, }} /> {/* Add Certificate Modal */} { setModalVisible(false); setSelectedCertificate(null); }} footer={null} width={800} >
{/* SSL Type and Cluster Selection */} prevValues.ssl_type !== currentValues.ssl_type}> {({ getFieldValue }) => { const sslType = getFieldValue('ssl_type'); return ( { if (sslType === 'cluster' && (!value || value.length === 0)) { return Promise.reject('Please select at least one cluster'); } return Promise.resolve(); } } ]} > ); }}