Files
AyposWeb/src/pages/MonitoringSystem.tsx

601 lines
18 KiB
TypeScript

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<string, string>;
}
interface ApiResponse {
optimization_space: Record<string, ComputeNode>;
}
// 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<MonitoringSystemProps> = ({
onSave,
isDialog = false,
initialBlockList = [],
initialSelectedVMs = [],
}) => {
const [expanded, setExpanded] = useState<string[]>(['org-main', 'region-main', 'dc-old-lab']);
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [treeData, setTreeData] = useState<TreeNode[]>([]);
const [isViewMode, setIsViewMode] = useState(false);
const [selectedVMs, setSelectedVMs] = useState<string[]>(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<string>();
// 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 <BusinessIcon color="primary" />;
case 'region':
return <LocationOnIcon color="primary" />;
case 'datacenter':
return <DnsIcon color="primary" />;
case 'pm':
return <ComputerIcon color="primary" />;
case 'vm':
return <MemoryIcon color="primary" />;
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 (
<Box key={node.id} sx={{ ml: level * 3 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
p: 0.5,
'&:hover': { bgcolor: 'action.hover' },
borderRadius: 1,
}}
>
{hasChildren && (
<IconButton
size="small"
onClick={() => handleNodeToggle(node.id)}
sx={{ mr: 1 }}
>
{isExpanded ? <ExpandMoreIcon /> : <ChevronRightIcon />}
</IconButton>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getNodeIcon(node.type)}
<Typography variant="body2">{node.name}</Typography>
<Switch
checked={isSelected}
onChange={() => 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',
},
}}
/>
</Box>
</Box>
{hasChildren && (
<Collapse in={isExpanded}>
<Box>
{node.children!.map(child => renderTreeNode(child, level + 1))}
</Box>
</Collapse>
)}
</Box>
);
};
// 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 (
<Box sx={{
height: isDialog ? 'auto' : '100vh',
bgcolor: 'background.default',
p: 3
}}>
{/* Header */}
<AppBar
position="static"
elevation={0}
sx={{
bgcolor: 'background.paper',
borderBottom: 1,
borderColor: 'divider'
}}
>
<Toolbar sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant="h5" color="textPrimary" sx={{ flex: 1 }}>
Optimization Space Selection
</Typography>
{isDialog && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mr: 2 }}>
<Button
variant={isViewMode ? "contained" : "outlined"}
onClick={() => setIsViewMode(true)}
size="small"
>
View
</Button>
<Button
variant={!isViewMode ? "contained" : "outlined"}
onClick={() => setIsViewMode(false)}
size="small"
>
Edit
</Button>
</Box>
)}
<Tooltip title="Save selected nodes">
<span>
<Button
startIcon={<SaveIcon />}
variant="contained"
onClick={handleSave}
disabled={loading || isViewMode}
sx={{ mr: 1 }}
>
Save
</Button>
</span>
</Tooltip>
<Tooltip title="Refresh tree">
<IconButton onClick={fetchData} disabled={loading}>
{loading ? <CircularProgress size={24} /> : <RefreshIcon />}
</IconButton>
</Tooltip>
</Toolbar>
</AppBar>
{/* Main Content */}
<Paper
elevation={0}
sx={{
p: 2,
height: '100%',
border: 1,
borderColor: 'divider',
borderRadius: 2,
bgcolor: 'background.paper',
mt: 2
}}
>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
<CircularProgress />
</Box>
) : (
<Box sx={{ mt: 1 }}>
{treeData.map(node => renderTreeNode(node))}
</Box>
)}
</Paper>
<Snackbar
open={alert.open}
autoHideDuration={6000}
onClose={() => setAlert({ ...alert, open: false })}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={() => setAlert({ ...alert, open: false })}
severity={alert.severity}
variant="filled"
sx={{ width: '100%' }}
>
{alert.message}
</Alert>
</Snackbar>
</Box>
);
};
export default MonitoringSystem;