import { useState, useEffect, useCallback } from 'react'; import { Box, Paper, Typography, IconButton, Switch, AppBar, Toolbar, Button, CircularProgress, Tooltip, Collapse, Grid, useTheme, styled, Chip, FormControl, InputLabel, Select, MenuItem, FormControlLabel, Alert, Snackbar, Tabs, Tab, } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import BusinessIcon from '@mui/icons-material/Business'; import LocationOnIcon from '@mui/icons-material/LocationOn'; import DnsIcon from '@mui/icons-material/Dns'; import ComputerIcon from '@mui/icons-material/Computer'; import MemoryIcon from '@mui/icons-material/Memory'; import RefreshIcon from '@mui/icons-material/Refresh'; import SaveIcon from '@mui/icons-material/Save'; import SpeedIcon from '@mui/icons-material/Speed'; import { stressService } from '../services/stressService'; // Define the structure of our tree nodes interface TreeNode { id: string; name: string; type: 'organization' | 'region' | 'datacenter' | 'pm' | 'vm' | 'compute'; children?: TreeNode[]; ip?: string; } interface ComputeNode { host_ip: string; hosted_vms: Record; } interface ApiResponse { optimization_space: Record; } // Helper function to get all descendant node IDs const getDescendantIds = (node: TreeNode): string[] => { let ids: string[] = [node.id]; if (node.children) { node.children.forEach(child => { ids = [...ids, ...getDescendantIds(child)]; }); } return ids; }; // Helper function to get all ancestor node IDs const getAncestorIds = (nodeId: string, node: TreeNode): string[] => { if (!node) return []; if (node.id === nodeId) return [node.id]; if (node.children) { for (const child of node.children) { const path = getAncestorIds(nodeId, child); if (path.length > 0) { return [node.id, ...path]; } } } return []; }; // Helper function to check if all children are selected const areAllChildrenSelected = (node: TreeNode, selectedNodes: string[]): boolean => { if (!node.children) return true; return node.children.every(child => { if (child.children) { return areAllChildrenSelected(child, selectedNodes); } return selectedNodes.includes(child.id); }); }; // Add new styled components for stress testing const StressTestingCard = styled(Paper)(({ theme }) => ({ padding: theme.spacing(3), borderRadius: theme.spacing(2), backgroundColor: theme.palette.background.paper, marginBottom: theme.spacing(3), })); const StressLevelChip = styled(Chip)<{ level: 'low' | 'medium' | 'high' }>(({ theme, level }) => ({ borderRadius: theme.spacing(1), fontWeight: 500, backgroundColor: level === 'low' ? theme.palette.success.light : level === 'medium' ? theme.palette.warning.light : theme.palette.error.light, color: level === 'low' ? theme.palette.success.dark : level === 'medium' ? theme.palette.warning.dark : theme.palette.error.dark, })); interface MonitoringSystemProps { onSave?: (unselectedVMs: string[], selectedVMs: string[]) => void; isDialog?: boolean; initialBlockList?: string[]; initialSelectedVMs?: string[]; } const MonitoringSystem: React.FC = ({ onSave, isDialog = false, initialBlockList = [], initialSelectedVMs = [], }) => { const [expanded, setExpanded] = useState(['org-main', 'region-main', 'dc-old-lab']); const [selectedNodes, setSelectedNodes] = useState([]); const [loading, setLoading] = useState(false); const [treeData, setTreeData] = useState([]); const [isViewMode, setIsViewMode] = useState(false); const [selectedVMs, setSelectedVMs] = useState(initialSelectedVMs); const [activeTab, setActiveTab] = useState(0); const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' | 'info' }>({ open: false, message: '', severity: 'info', }); // Fetch data and initialize state const fetchData = async () => { setLoading(true); try { const response = await fetch('http://10.150.1.167:8003/prom/monitoring'); const result: ApiResponse = await response.json(); // Create hierarchical structure const hierarchicalData: TreeNode[] = [ { id: 'org-main', name: 'Main Organization', type: 'organization', children: [ { id: 'region-main', name: 'Region', type: 'region', children: [ { id: 'dc-ulak', name: 'Ulak', type: 'datacenter', children: [] // Empty for now }, { id: 'dc-old-lab', name: 'Old Lab', type: 'datacenter', children: Object.entries(result.optimization_space).map(([computeName, computeData]) => ({ id: computeName, name: computeName, type: 'compute', ip: computeData.host_ip, children: Object.entries(computeData.hosted_vms).map(([vmName, vmIp]) => ({ id: `${computeName}-${vmName}`, name: vmName, type: 'vm', ip: vmIp })) })) }, { id: 'dc-new-lab', name: 'New Lab', type: 'datacenter', children: [] // Empty for now } ] } ] } ]; setTreeData(hierarchicalData); // Initialize selection based on initial values only if they exist if (initialBlockList.length > 0 || initialSelectedVMs.length > 0) { const blockList = initialBlockList; // Select nodes that are not in the block list const nodesToSelect = new Set(); // Helper function to process compute nodes const processComputeNodes = (nodes: TreeNode[]) => { nodes.forEach(node => { if (node.type === 'compute') { let hasSelectedVM = false; // Check compute node if (node.ip && !blockList.includes(node.ip)) { nodesToSelect.add(node.id); } // Check VM nodes node.children?.forEach(vm => { if (vm.ip && !blockList.includes(vm.ip)) { nodesToSelect.add(vm.id); hasSelectedVM = true; } }); // If any VM is selected, ensure the compute is selected too if (hasSelectedVM) { nodesToSelect.add(node.id); } } // Recursively process children if (node.children) { processComputeNodes(node.children); } }); }; // Process all nodes processComputeNodes(hierarchicalData); // Set the selected nodes setSelectedNodes(Array.from(nodesToSelect)); } // Expand organization and region nodes by default setExpanded(['org-main', 'region-main', 'dc-old-lab']); } catch (error) { console.error('Error fetching data:', error); } finally { setLoading(false); } }; // Initialize with previous state useEffect(() => { fetchData(); }, [initialBlockList, initialSelectedVMs]); // Re-fetch when initial values change // Get appropriate icon for each node type const getNodeIcon = (type: TreeNode['type']) => { switch (type) { case 'organization': return ; case 'region': return ; case 'datacenter': return ; case 'pm': return ; case 'vm': return ; default: return null; } }; // Handle node expansion const handleNodeToggle = (nodeId: string) => { setExpanded(prev => { const isExpanded = prev.includes(nodeId); if (isExpanded) { return prev.filter(id => id !== nodeId); } else { return [...prev, nodeId]; } }); }; // Updated node selection handler for toggle-like selection with parent-child association const handleNodeSelect = (nodeId: string) => { setSelectedNodes(prev => { const isSelected = prev.includes(nodeId); let newSelected = [...prev]; // Find the node in the tree const findNode = (nodes: TreeNode[]): TreeNode | null => { for (const node of nodes) { if (node.id === nodeId) return node; if (node.children) { const found = findNode(node.children); if (found) return found; } } return null; }; // Find the parent compute node for a VM const findParentCompute = (nodes: TreeNode[], vmId: string): TreeNode | null => { for (const node of nodes) { if (node.type === 'compute' && node.children?.some(vm => vm.id === vmId)) { return node; } if (node.children) { const found = findParentCompute(node.children, vmId); if (found) return found; } } return null; }; const targetNode = findNode(treeData); if (!targetNode) return prev; if (isSelected) { // When deselecting a node if (targetNode.type === 'compute') { // If deselecting a compute, deselect all its VMs const computeAndVMs = [targetNode.id, ...(targetNode.children?.map(vm => vm.id) || [])]; newSelected = newSelected.filter(id => !computeAndVMs.includes(id)); } else if (targetNode.type === 'vm') { // If deselecting a VM, just deselect it newSelected = newSelected.filter(id => id !== nodeId); // If this was the last VM, deselect the parent compute too const parentCompute = findParentCompute(treeData, nodeId); if (parentCompute) { const siblingVMs = parentCompute.children?.filter(vm => vm.id !== nodeId) || []; const hasSelectedSiblings = siblingVMs.some(vm => newSelected.includes(vm.id)); if (!hasSelectedSiblings) { newSelected = newSelected.filter(id => id !== parentCompute.id); } } } } else { // When selecting a node if (targetNode.type === 'compute') { // If selecting a compute, select all its VMs newSelected.push(targetNode.id); targetNode.children?.forEach(vm => { newSelected.push(vm.id); }); } else if (targetNode.type === 'vm') { // If selecting a VM, select it and its parent compute newSelected.push(nodeId); const parentCompute = findParentCompute(treeData, nodeId); if (parentCompute) { newSelected.push(parentCompute.id); } } } // Remove duplicates and return return Array.from(new Set(newSelected)); }); }; // Updated render function with disabled switches in view mode const renderTreeNode = (node: TreeNode, level: number = 0) => { const isExpanded = expanded.includes(node.id); const hasChildren = node.children && node.children.length > 0; const isSelected = selectedNodes.includes(node.id); return ( {hasChildren && ( handleNodeToggle(node.id)} sx={{ mr: 1 }} > {isExpanded ? : } )} {getNodeIcon(node.type)} {node.name} handleNodeSelect(node.id)} disabled={isViewMode} size="small" sx={{ ml: 1, '& .MuiSwitch-switchBase.Mui-checked': { color: '#4caf50', }, '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { backgroundColor: '#4caf50', }, '& .MuiSwitch-track': { backgroundColor: '#bdbdbd', }, }} /> {hasChildren && ( {node.children!.map(child => renderTreeNode(child, level + 1))} )} ); }; // Get unselected and selected VMs including compute IPs const getVMSelectionStatus = () => { const allIPs: string[] = []; const selectedIPs: string[] = []; // Find the Old Lab datacenter node that contains the dynamic data const oldLabNode = treeData[0]?.children?.[0]?.children?.find(node => node.id === 'dc-old-lab'); if (!oldLabNode) return { selectedVMs: [], unselectedVMs: [] }; // Process only the compute nodes in Old Lab oldLabNode.children?.forEach(compute => { // Add compute IP if (compute.ip) { allIPs.push(compute.ip); if (selectedNodes.includes(compute.id)) { selectedIPs.push(compute.ip); } } // Add VM IPs compute.children?.forEach(vm => { if (vm.ip) { allIPs.push(vm.ip); if (selectedNodes.includes(vm.id)) { selectedIPs.push(vm.ip); } } }); }); // Calculate unselected IPs for block list const unselectedIPs = allIPs.filter(ip => !selectedIPs.includes(ip)); console.log('Block list IPs:', unselectedIPs); return { selectedVMs: selectedIPs, unselectedVMs: unselectedIPs }; }; // Handle save action const handleSave = async () => { try { setLoading(true); const { selectedVMs, unselectedVMs } = getVMSelectionStatus(); console.log('Selected VMs and Computes:', selectedVMs); console.log('Unselected VMs and Computes:', unselectedVMs); // Store selected VMs in localStorage for stress testing const oldLabNode = treeData[0]?.children?.[0]?.children?.find(node => node.id === 'dc-old-lab'); if (oldLabNode) { const selectedVMObjects = oldLabNode.children?.flatMap(compute => compute.children?.filter(vm => selectedNodes.includes(vm.id)) .map(vm => ({ id: vm.id, name: vm.name, ip: vm.ip })) || [] ) || []; console.log('Storing VMs in localStorage:', selectedVMObjects); localStorage.setItem('stressTestVMs', JSON.stringify(selectedVMObjects)); } if (onSave) { onSave(unselectedVMs, selectedVMs); } } catch (error) { console.error('Error saving selection:', error); } finally { setLoading(false); } }; return ( {/* Header */} Optimization Space Selection {isDialog && ( )} {loading ? : } {/* Main Content */} {loading ? ( ) : ( {treeData.map(node => renderTreeNode(node))} )} setAlert({ ...alert, open: false })} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > setAlert({ ...alert, open: false })} severity={alert.severity} variant="filled" sx={{ width: '100%' }} > {alert.message} ); }; export default MonitoringSystem;