forked from BLC/AyposWeb
601 lines
18 KiB
TypeScript
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;
|