586 lines
18 KiB
TypeScript
586 lines
18 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Card,
|
|
CardContent,
|
|
Typography,
|
|
LinearProgress,
|
|
Chip,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
ListItemIcon,
|
|
Avatar,
|
|
IconButton,
|
|
Alert,
|
|
Button,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Badge,
|
|
Tabs,
|
|
Tab,
|
|
Paper,
|
|
Grid,
|
|
} from '@mui/material';
|
|
import {
|
|
BugReport as IncidentIcon,
|
|
Schedule as SLIcon,
|
|
Security as SecurityIcon,
|
|
Warning as WarningIcon,
|
|
CheckCircle as CheckCircleIcon,
|
|
Error as ErrorIcon,
|
|
TrendingUp as TrendingUpIcon,
|
|
TrendingDown as TrendingDownIcon,
|
|
Refresh as RefreshIcon,
|
|
Notifications as NotificationsIcon,
|
|
Dashboard as DashboardIcon,
|
|
ViewModule as ModuleIcon,
|
|
} from '@mui/icons-material';
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import apiService from '../../services/api';
|
|
import { Incident, Alert as AlertType, OnCallAssignment } from '../../types';
|
|
import ModuleOverviewCards from './ModuleOverviewCards';
|
|
|
|
interface DashboardStats {
|
|
totalIncidents: number;
|
|
openIncidents: number;
|
|
resolvedIncidents: number;
|
|
criticalIncidents: number;
|
|
slaBreaches: number;
|
|
activeAlerts: number;
|
|
systemHealth: number;
|
|
mttr: number;
|
|
mtta: number;
|
|
}
|
|
|
|
|
|
const OverviewDashboard: React.FC = () => {
|
|
const { user } = useAuth();
|
|
const navigate = useNavigate();
|
|
const [activeTab, setActiveTab] = useState(0);
|
|
const [stats, setStats] = useState<DashboardStats>({
|
|
totalIncidents: 0,
|
|
openIncidents: 0,
|
|
resolvedIncidents: 0,
|
|
criticalIncidents: 0,
|
|
slaBreaches: 0,
|
|
activeAlerts: 0,
|
|
systemHealth: 100,
|
|
mttr: 0,
|
|
mtta: 0,
|
|
});
|
|
const [recentIncidents, setRecentIncidents] = useState<Incident[]>([]);
|
|
const [recentAlerts, setRecentAlerts] = useState<AlertType[]>([]);
|
|
const [currentOnCall, setCurrentOnCall] = useState<OnCallAssignment | null>(null);
|
|
// const [systemMetrics] = useState<SystemMetric[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
loadDashboardData();
|
|
}, []);
|
|
|
|
const loadDashboardData = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
// Load incidents
|
|
const incidentsResponse = await apiService.getIncidents({ page_size: 10 });
|
|
setRecentIncidents(incidentsResponse.results);
|
|
|
|
// Load alerts
|
|
const alertsResponse = await apiService.getAlerts({ page_size: 5 });
|
|
setRecentAlerts(alertsResponse.results);
|
|
|
|
// Load current on-call
|
|
const onCallResponse = await apiService.getCurrentOnCall();
|
|
setCurrentOnCall(onCallResponse);
|
|
|
|
// Load system metrics
|
|
// const metricsResponse = await apiService.getSystemMetrics();
|
|
// setSystemMetrics(metricsResponse); // Commented out as it's not used
|
|
|
|
// Calculate stats
|
|
const totalIncidents = incidentsResponse.count;
|
|
const openIncidents = incidentsResponse.results.filter(i => i.status === 'OPEN' || i.status === 'IN_PROGRESS').length;
|
|
const resolvedIncidents = incidentsResponse.results.filter(i => i.status === 'RESOLVED' || i.status === 'CLOSED').length;
|
|
const criticalIncidents = incidentsResponse.results.filter(i => i.severity === 'CRITICAL' || i.severity === 'EMERGENCY').length;
|
|
const activeAlerts = alertsResponse.results.filter(a => a.status === 'TRIGGERED').length;
|
|
|
|
setStats({
|
|
totalIncidents,
|
|
openIncidents,
|
|
resolvedIncidents,
|
|
criticalIncidents,
|
|
slaBreaches: 0, // This would come from SLA API
|
|
activeAlerts,
|
|
systemHealth: 95, // This would be calculated from health checks
|
|
mttr: 2.5, // This would come from analytics
|
|
mtta: 0.5, // This would come from analytics
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load dashboard data:', error);
|
|
setError('Failed to load dashboard data');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const getSeverityColor = (severity: string) => {
|
|
switch (severity) {
|
|
case 'CRITICAL':
|
|
case 'EMERGENCY':
|
|
return 'error';
|
|
case 'HIGH':
|
|
return 'warning';
|
|
case 'MEDIUM':
|
|
return 'info';
|
|
case 'LOW':
|
|
return 'success';
|
|
default:
|
|
return 'default';
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'OPEN':
|
|
return 'error';
|
|
case 'IN_PROGRESS':
|
|
return 'warning';
|
|
case 'RESOLVED':
|
|
return 'success';
|
|
case 'CLOSED':
|
|
return 'default';
|
|
default:
|
|
return 'default';
|
|
}
|
|
};
|
|
|
|
const formatTime = (timestamp: string) => {
|
|
return new Date(timestamp).toLocaleString();
|
|
};
|
|
|
|
const handleModuleClick = (moduleId: string) => {
|
|
// Navigate to the specific module page
|
|
const moduleRoutes: { [key: string]: string } = {
|
|
incident_intelligence: '/incidents',
|
|
security: '/security',
|
|
monitoring: '/monitoring',
|
|
sla_oncall: '/sla',
|
|
automation_orchestration: '/automation',
|
|
collaboration_war_rooms: '/collaboration',
|
|
analytics_predictive_insights: '/analytics',
|
|
knowledge_learning: '/knowledge',
|
|
compliance_governance: '/compliance',
|
|
};
|
|
|
|
const route = moduleRoutes[moduleId];
|
|
if (route) {
|
|
navigate(route);
|
|
}
|
|
};
|
|
|
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
|
setActiveTab(newValue);
|
|
};
|
|
|
|
const StatCard: React.FC<{
|
|
title: string;
|
|
value: string | number;
|
|
icon: React.ReactNode;
|
|
color: string;
|
|
trend?: 'up' | 'down' | 'neutral';
|
|
trendValue?: string;
|
|
}> = ({ title, value, icon, color, trend, trendValue }) => (
|
|
<Card>
|
|
<CardContent>
|
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
|
<Box>
|
|
<Typography color="textSecondary" gutterBottom variant="h6">
|
|
{title}
|
|
</Typography>
|
|
<Typography variant="h4" component="div" color={color}>
|
|
{value}
|
|
</Typography>
|
|
{trend && trendValue && (
|
|
<Box display="flex" alignItems="center" mt={1}>
|
|
{trend === 'up' ? (
|
|
<TrendingUpIcon color="error" fontSize="small" />
|
|
) : trend === 'down' ? (
|
|
<TrendingDownIcon color="success" fontSize="small" />
|
|
) : null}
|
|
<Typography variant="caption" color="textSecondary" sx={{ ml: 0.5 }}>
|
|
{trendValue}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
<Avatar sx={{ bgcolor: `${color}.main` }}>
|
|
{icon}
|
|
</Avatar>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Box>
|
|
<LinearProgress />
|
|
<Typography variant="h6" sx={{ mt: 2 }}>
|
|
Loading dashboard...
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Alert severity="error" action={
|
|
<Button color="inherit" size="small" onClick={loadDashboardData}>
|
|
Retry
|
|
</Button>
|
|
}>
|
|
{error}
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
{/* Welcome Section */}
|
|
<Box mb={3}>
|
|
<Typography variant="h4" gutterBottom>
|
|
Welcome back, {user?.first_name || user?.username}!
|
|
</Typography>
|
|
<Typography variant="subtitle1" color="textSecondary">
|
|
Enterprise Incident Management Dashboard - Real-time system overview
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* Dashboard Tabs */}
|
|
<Paper sx={{ mb: 3 }}>
|
|
<Tabs
|
|
value={activeTab}
|
|
onChange={handleTabChange}
|
|
indicatorColor="primary"
|
|
textColor="primary"
|
|
>
|
|
<Tab
|
|
icon={<DashboardIcon />}
|
|
label="Dashboard Overview"
|
|
iconPosition="start"
|
|
/>
|
|
<Tab
|
|
icon={<ModuleIcon />}
|
|
label="Module Status"
|
|
iconPosition="start"
|
|
/>
|
|
</Tabs>
|
|
</Paper>
|
|
|
|
{/* Tab Content */}
|
|
{activeTab === 0 && (
|
|
<>
|
|
|
|
{/* Stats Cards */}
|
|
<Grid container spacing={3} mb={3}>
|
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
|
<StatCard
|
|
title="Total Incidents"
|
|
value={stats.totalIncidents}
|
|
icon={<IncidentIcon />}
|
|
color="primary"
|
|
trend="up"
|
|
trendValue="+12% from last week"
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
|
<StatCard
|
|
title="Open Incidents"
|
|
value={stats.openIncidents}
|
|
icon={<WarningIcon />}
|
|
color="warning"
|
|
trend="down"
|
|
trendValue="-5% from yesterday"
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
|
<StatCard
|
|
title="Critical Issues"
|
|
value={stats.criticalIncidents}
|
|
icon={<ErrorIcon />}
|
|
color="error"
|
|
trend="neutral"
|
|
trendValue="No change"
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
|
<StatCard
|
|
title="System Health"
|
|
value={`${stats.systemHealth}%`}
|
|
icon={<CheckCircleIcon />}
|
|
color="success"
|
|
trend="up"
|
|
trendValue="+2% from last hour"
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{/* Performance Metrics */}
|
|
<Grid container spacing={3} mb={3}>
|
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
|
<StatCard
|
|
title="MTTR (hours)"
|
|
value={stats.mttr}
|
|
icon={<SLIcon />}
|
|
color="info"
|
|
trend="down"
|
|
trendValue="-0.5h from last week"
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
|
<StatCard
|
|
title="MTTA (hours)"
|
|
value={stats.mtta}
|
|
icon={<NotificationsIcon />}
|
|
color="secondary"
|
|
trend="down"
|
|
trendValue="-0.2h from last week"
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
|
<StatCard
|
|
title="Active Alerts"
|
|
value={stats.activeAlerts}
|
|
icon={<SecurityIcon />}
|
|
color="warning"
|
|
trend="up"
|
|
trendValue="+3 from yesterday"
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
|
<StatCard
|
|
title="SLA Breaches"
|
|
value={stats.slaBreaches}
|
|
icon={<ErrorIcon />}
|
|
color="error"
|
|
trend="down"
|
|
trendValue="-1 from last week"
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{/* Main Content Grid */}
|
|
<Grid container spacing={3}>
|
|
{/* Recent Incidents */}
|
|
<Grid size={{ xs: 12, md: 8 }}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
|
<Typography variant="h6">
|
|
Recent Incidents
|
|
</Typography>
|
|
<IconButton onClick={loadDashboardData}>
|
|
<RefreshIcon />
|
|
</IconButton>
|
|
</Box>
|
|
<TableContainer>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Title</TableCell>
|
|
<TableCell>Severity</TableCell>
|
|
<TableCell>Status</TableCell>
|
|
<TableCell>Assigned To</TableCell>
|
|
<TableCell>Created</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{recentIncidents.map((incident) => (
|
|
<TableRow key={incident.id}>
|
|
<TableCell>
|
|
<Typography variant="body2" noWrap>
|
|
{incident.title}
|
|
</Typography>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Chip
|
|
label={incident.severity}
|
|
color={getSeverityColor(incident.severity) as any}
|
|
size="small"
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Chip
|
|
label={incident.status}
|
|
color={getStatusColor(incident.status) as any}
|
|
size="small"
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
{incident.assigned_to ? (
|
|
<Box display="flex" alignItems="center">
|
|
<Avatar sx={{ width: 24, height: 24, mr: 1 }}>
|
|
{incident.assigned_to.first_name?.[0]}
|
|
</Avatar>
|
|
<Typography variant="body2">
|
|
{incident.assigned_to.first_name} {incident.assigned_to.last_name}
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<Typography variant="body2" color="textSecondary">
|
|
Unassigned
|
|
</Typography>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Typography variant="body2">
|
|
{formatTime(incident.created_at)}
|
|
</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Sidebar */}
|
|
<Grid size={{ xs: 12, md: 4 }}>
|
|
{/* Current On-Call */}
|
|
{currentOnCall && (
|
|
<Card sx={{ mb: 3 }}>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
Current On-Call
|
|
</Typography>
|
|
<Box display="flex" alignItems="center">
|
|
<Avatar sx={{ mr: 2 }}>
|
|
{currentOnCall.user.first_name?.[0]}
|
|
</Avatar>
|
|
<Box>
|
|
<Typography variant="subtitle1">
|
|
{currentOnCall.user.first_name} {currentOnCall.user.last_name}
|
|
</Typography>
|
|
<Typography variant="body2" color="textSecondary">
|
|
{currentOnCall.rotation.team_name}
|
|
</Typography>
|
|
<Typography variant="caption" color="textSecondary">
|
|
Until {formatTime(currentOnCall.end_time)}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Recent Alerts */}
|
|
<Card sx={{ mb: 3 }}>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
Recent Alerts
|
|
</Typography>
|
|
<List dense>
|
|
{recentAlerts.map((alert) => (
|
|
<ListItem key={alert.id}>
|
|
<ListItemIcon>
|
|
<Badge color="error" variant="dot">
|
|
<NotificationsIcon />
|
|
</Badge>
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary={alert.title}
|
|
secondary={
|
|
<Box>
|
|
<Typography variant="caption" display="block">
|
|
{formatTime(alert.triggered_at)}
|
|
</Typography>
|
|
<Chip
|
|
label={alert.severity}
|
|
color={getSeverityColor(alert.severity) as any}
|
|
size="small"
|
|
/>
|
|
</Box>
|
|
}
|
|
/>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* System Status */}
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
System Status
|
|
</Typography>
|
|
<Box mb={2}>
|
|
<Box display="flex" justifyContent="space-between" mb={1}>
|
|
<Typography variant="body2">Overall Health</Typography>
|
|
<Typography variant="body2">{stats.systemHealth}%</Typography>
|
|
</Box>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={stats.systemHealth}
|
|
color={stats.systemHealth > 90 ? 'success' : stats.systemHealth > 70 ? 'warning' : 'error'}
|
|
/>
|
|
</Box>
|
|
|
|
<List dense>
|
|
<ListItem>
|
|
<ListItemIcon>
|
|
<CheckCircleIcon color="success" />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary="API Services"
|
|
secondary="Operational"
|
|
/>
|
|
</ListItem>
|
|
<ListItem>
|
|
<ListItemIcon>
|
|
<CheckCircleIcon color="success" />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary="Database"
|
|
secondary="Operational"
|
|
/>
|
|
</ListItem>
|
|
<ListItem>
|
|
<ListItemIcon>
|
|
<WarningIcon color="warning" />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary="Monitoring"
|
|
secondary="Degraded"
|
|
/>
|
|
</ListItem>
|
|
</List>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
</Grid>
|
|
</>
|
|
)}
|
|
|
|
{/* Module Status Tab */}
|
|
{activeTab === 1 && (
|
|
<ModuleOverviewCards onModuleClick={handleModuleClick} />
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default OverviewDashboard;
|