Updates
This commit is contained in:
1
etb-dashboard/.env
Normal file
1
etb-dashboard/.env
Normal file
@@ -0,0 +1 @@
|
||||
REACT_APP_API_URL=http://localhost:8000
|
||||
23
etb-dashboard/.gitignore
vendored
Normal file
23
etb-dashboard/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
46
etb-dashboard/README.md
Normal file
46
etb-dashboard/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
18422
etb-dashboard/package-lock.json
generated
Normal file
18422
etb-dashboard/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
63
etb-dashboard/package.json
Normal file
63
etb-dashboard/package.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "etb-dashboard",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@mui/icons-material": "^7.3.2",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@mui/x-data-grid": "^8.11.3",
|
||||
"@mui/x-date-pickers": "^8.11.3",
|
||||
"@tanstack/react-query": "^5.89.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.126",
|
||||
"@types/qrcode.react": "^1.0.5",
|
||||
"@types/react": "^19.1.13",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"axios": "^1.12.2",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.9.36",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.17"
|
||||
}
|
||||
}
|
||||
6
etb-dashboard/postcss.config.js
Normal file
6
etb-dashboard/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
38
etb-dashboard/public/etb-logo.svg
Normal file
38
etb-dashboard/public/etb-logo.svg
Normal file
@@ -0,0 +1,38 @@
|
||||
<svg width="200" height="60" viewBox="0 0 200 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background Shield -->
|
||||
<defs>
|
||||
<linearGradient id="shieldGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#1e3c72;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#2a5298;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#1e3c72;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#2a5298;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Shield Icon -->
|
||||
<path d="M20 10 L20 25 C20 30 25 35 30 35 L30 40 C30 45 35 50 40 50 L40 45 C45 45 50 40 50 35 L50 10 Z"
|
||||
fill="url(#shieldGradient)"
|
||||
stroke="#ffffff"
|
||||
stroke-width="2"/>
|
||||
|
||||
<!-- Security Symbol -->
|
||||
<circle cx="35" cy="25" r="8" fill="#ffffff" opacity="0.9"/>
|
||||
<path d="M32 25 L34 27 L38 23" stroke="#1e3c72" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
<!-- ETB Text -->
|
||||
<text x="70" y="25" font-family="Arial, sans-serif" font-size="24" font-weight="bold" fill="url(#textGradient)">
|
||||
ETB Security
|
||||
</text>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<text x="70" y="40" font-family="Arial, sans-serif" font-size="12" fill="#666666">
|
||||
Enterprise Threat & Breach Management
|
||||
</text>
|
||||
|
||||
<!-- Decorative Elements -->
|
||||
<circle cx="180" cy="15" r="3" fill="#1e3c72" opacity="0.3"/>
|
||||
<circle cx="185" cy="25" r="2" fill="#2a5298" opacity="0.4"/>
|
||||
<circle cx="175" cy="35" r="2.5" fill="#1e3c72" opacity="0.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
etb-dashboard/public/favicon.ico
Normal file
BIN
etb-dashboard/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
43
etb-dashboard/public/index.html
Normal file
43
etb-dashboard/public/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
etb-dashboard/public/logo192.png
Normal file
BIN
etb-dashboard/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
etb-dashboard/public/logo512.png
Normal file
BIN
etb-dashboard/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
etb-dashboard/public/manifest.json
Normal file
25
etb-dashboard/public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
etb-dashboard/public/robots.txt
Normal file
3
etb-dashboard/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
38
etb-dashboard/src/App.css
Normal file
38
etb-dashboard/src/App.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
9
etb-dashboard/src/App.test.tsx
Normal file
9
etb-dashboard/src/App.test.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
341
etb-dashboard/src/App.tsx
Normal file
341
etb-dashboard/src/App.tsx
Normal file
@@ -0,0 +1,341 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
// Contexts
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
|
||||
// Components
|
||||
import LoginPage from './components/Login/LoginPage';
|
||||
import DashboardLayout from './components/Dashboard/DashboardLayout';
|
||||
import OverviewDashboard from './components/Dashboard/OverviewDashboard';
|
||||
import IncidentIntelligenceDashboard from './components/Dashboard/IncidentIntelligenceDashboard';
|
||||
import SecurityDashboard from './components/Dashboard/SecurityDashboard';
|
||||
import MonitoringDashboard from './components/Dashboard/MonitoringDashboard';
|
||||
import SLAOnCallDashboard from './components/Dashboard/SLAOnCallDashboard';
|
||||
import AutomationDashboard from './components/Dashboard/AutomationDashboard';
|
||||
import CollaborationDashboard from './components/Dashboard/CollaborationDashboard';
|
||||
import AnalyticsDashboard from './components/Dashboard/AnalyticsDashboard';
|
||||
import KnowledgeDashboard from './components/Dashboard/KnowledgeDashboard';
|
||||
import ComplianceDashboard from './components/Dashboard/ComplianceDashboard';
|
||||
import UserManagementDashboard from './components/Dashboard/UserManagementDashboard';
|
||||
|
||||
// Create a custom theme
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: {
|
||||
main: '#1976d2',
|
||||
light: '#42a5f5',
|
||||
dark: '#1565c0',
|
||||
},
|
||||
secondary: {
|
||||
main: '#dc004e',
|
||||
light: '#ff5983',
|
||||
dark: '#9a0036',
|
||||
},
|
||||
background: {
|
||||
default: '#f5f5f5',
|
||||
paper: '#ffffff',
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||
h1: {
|
||||
fontSize: '2.5rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h2: {
|
||||
fontSize: '2rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h3: {
|
||||
fontSize: '1.75rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h4: {
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h5: {
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h6: {
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
borderRadius: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 12,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create a client for React Query
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Protected Route Component
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
||||
if (!token) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
// Placeholder components for other routes
|
||||
const IncidentsPage: React.FC = () => (
|
||||
<IncidentIntelligenceDashboard onNavigateToModule={(moduleId) => {
|
||||
// Handle navigation to other modules
|
||||
console.log('Navigate to module:', moduleId);
|
||||
}} />
|
||||
);
|
||||
|
||||
const MonitoringPage: React.FC = () => (
|
||||
<MonitoringDashboard onNavigateToModule={(moduleId) => {
|
||||
// Handle navigation to other modules
|
||||
console.log('Navigate to module:', moduleId);
|
||||
}} />
|
||||
);
|
||||
|
||||
const SLAPage: React.FC = () => (
|
||||
<SLAOnCallDashboard onNavigateToModule={(moduleId) => {
|
||||
// Handle navigation to other modules
|
||||
console.log('Navigate to module:', moduleId);
|
||||
}} />
|
||||
);
|
||||
|
||||
const SecurityPage: React.FC = () => (
|
||||
<SecurityDashboard onNavigateToModule={(moduleId) => {
|
||||
// Handle navigation to other modules
|
||||
console.log('Navigate to module:', moduleId);
|
||||
}} />
|
||||
);
|
||||
|
||||
const AutomationPage: React.FC = () => (
|
||||
<AutomationDashboard onNavigateToModule={(moduleId) => {
|
||||
// Handle navigation to other modules
|
||||
console.log('Navigate to module:', moduleId);
|
||||
}} />
|
||||
);
|
||||
|
||||
const CollaborationPage: React.FC = () => (
|
||||
<CollaborationDashboard onNavigateToModule={(moduleId) => {
|
||||
// Handle navigation to other modules
|
||||
console.log('Navigate to module:', moduleId);
|
||||
}} />
|
||||
);
|
||||
|
||||
const AnalyticsPage: React.FC = () => (
|
||||
<AnalyticsDashboard onNavigateToModule={(moduleId) => {
|
||||
// Handle navigation to other modules
|
||||
console.log('Navigate to module:', moduleId);
|
||||
}} />
|
||||
);
|
||||
|
||||
const KnowledgePage: React.FC = () => (
|
||||
<KnowledgeDashboard onNavigateToModule={(moduleId) => {
|
||||
// Handle navigation to other modules
|
||||
console.log('Navigate to module:', moduleId);
|
||||
}} />
|
||||
);
|
||||
|
||||
const CompliancePage: React.FC = () => (
|
||||
<ComplianceDashboard onNavigateToModule={(moduleId) => {
|
||||
// Handle navigation to other modules
|
||||
console.log('Navigate to module:', moduleId);
|
||||
}} />
|
||||
);
|
||||
|
||||
const UserManagementPage: React.FC = () => (
|
||||
<UserManagementDashboard onNavigateToModule={(moduleId) => {
|
||||
// Handle navigation to other modules
|
||||
console.log('Navigate to module:', moduleId);
|
||||
}} />
|
||||
);
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
{/* Public Routes */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
|
||||
{/* Protected Routes */}
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<OverviewDashboard />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/incidents"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<IncidentsPage />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/monitoring"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<MonitoringPage />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/sla"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<SLAPage />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/security"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<SecurityPage />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/automation"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<AutomationPage />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/collaboration"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<CollaborationPage />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/analytics"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<AnalyticsPage />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/knowledge"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<KnowledgePage />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/compliance"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<CompliancePage />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/user-management"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<UserManagementPage />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Default redirect */}
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
|
||||
{/* Catch all route */}
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
861
etb-dashboard/src/components/Dashboard/AnalyticsDashboard.tsx
Normal file
861
etb-dashboard/src/components/Dashboard/AnalyticsDashboard.tsx
Normal file
@@ -0,0 +1,861 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Grid,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Button,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Avatar,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Settings as SettingsIcon,
|
||||
Timeline as TimelineIcon,
|
||||
Psychology as PsychologyIcon,
|
||||
Insights as InsightsIcon,
|
||||
Warning as WarningIcon,
|
||||
Speed as SpeedIcon,
|
||||
Download as DownloadIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface AnalyticsDashboardProps {
|
||||
onNavigateToModule: (moduleId: string) => void;
|
||||
}
|
||||
|
||||
interface AnalyticsStats {
|
||||
totalIncidents: number;
|
||||
avgResolutionTime: number;
|
||||
avgResponseTime: number;
|
||||
automationSuccessRate: number;
|
||||
predictionAccuracy: number;
|
||||
anomalyDetections: number;
|
||||
costSavings: number;
|
||||
trendPrediction: 'UP' | 'DOWN' | 'STABLE';
|
||||
}
|
||||
|
||||
interface PredictionModel {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: 'INCIDENT_PREDICTION' | 'SEVERITY_PREDICTION' | 'RESOLUTION_TIME' | 'ANOMALY_DETECTION';
|
||||
accuracy: number;
|
||||
lastTrained: string;
|
||||
status: 'ACTIVE' | 'TRAINING' | 'INACTIVE';
|
||||
predictionsToday: number;
|
||||
correctPredictions: number;
|
||||
}
|
||||
|
||||
interface AnomalyDetection {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
type: 'PERFORMANCE' | 'SECURITY' | 'BEHAVIORAL' | 'SYSTEM';
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
description: string;
|
||||
confidence: number;
|
||||
status: 'NEW' | 'INVESTIGATING' | 'RESOLVED' | 'FALSE_POSITIVE';
|
||||
relatedIncident?: string;
|
||||
affectedSystems: string[];
|
||||
}
|
||||
|
||||
interface CostImpactAnalysis {
|
||||
id: string;
|
||||
incidentId: string;
|
||||
incidentTitle: string;
|
||||
estimatedCost: number;
|
||||
actualCost: number;
|
||||
costCategory: 'DOWNTIME' | 'RESOURCE' | 'REPUTATION' | 'COMPLIANCE';
|
||||
analysisDate: string;
|
||||
mitigationActions: string[];
|
||||
}
|
||||
|
||||
interface TrendAnalysis {
|
||||
metric: string;
|
||||
currentValue: number;
|
||||
previousValue: number;
|
||||
changePercent: number;
|
||||
trend: 'UP' | 'DOWN' | 'STABLE';
|
||||
timeframe: string;
|
||||
prediction: number;
|
||||
}
|
||||
|
||||
const AnalyticsDashboard: React.FC<AnalyticsDashboardProps> = ({ onNavigateToModule }) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [stats, setStats] = useState<AnalyticsStats>({
|
||||
totalIncidents: 0,
|
||||
avgResolutionTime: 0,
|
||||
avgResponseTime: 0,
|
||||
automationSuccessRate: 0,
|
||||
predictionAccuracy: 0,
|
||||
anomalyDetections: 0,
|
||||
costSavings: 0,
|
||||
trendPrediction: 'STABLE',
|
||||
});
|
||||
const [predictionModels, setPredictionModels] = useState<PredictionModel[]>([]);
|
||||
const [anomalyDetections, setAnomalyDetections] = useState<AnomalyDetection[]>([]);
|
||||
const [costAnalyses, setCostAnalyses] = useState<CostImpactAnalysis[]>([]);
|
||||
const [trendAnalyses, setTrendAnalyses] = useState<TrendAnalysis[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadAnalyticsData();
|
||||
}, []);
|
||||
|
||||
const loadAnalyticsData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Mock data - replace with actual API calls
|
||||
setStats({
|
||||
totalIncidents: 156,
|
||||
avgResolutionTime: 2.3,
|
||||
avgResponseTime: 0.8,
|
||||
automationSuccessRate: 94.2,
|
||||
predictionAccuracy: 91.8,
|
||||
anomalyDetections: 23,
|
||||
costSavings: 125000,
|
||||
trendPrediction: 'DOWN',
|
||||
});
|
||||
|
||||
setPredictionModels([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Incident Severity Predictor',
|
||||
description: 'Predicts incident severity based on initial symptoms and patterns',
|
||||
type: 'SEVERITY_PREDICTION',
|
||||
accuracy: 94.2,
|
||||
lastTrained: '2024-01-15T06:00:00Z',
|
||||
status: 'ACTIVE',
|
||||
predictionsToday: 45,
|
||||
correctPredictions: 42,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Resolution Time Estimator',
|
||||
description: 'Estimates time to resolution based on incident characteristics',
|
||||
type: 'RESOLUTION_TIME',
|
||||
accuracy: 87.6,
|
||||
lastTrained: '2024-01-14T18:00:00Z',
|
||||
status: 'ACTIVE',
|
||||
predictionsToday: 38,
|
||||
correctPredictions: 33,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Anomaly Detection Engine',
|
||||
description: 'Detects unusual patterns in system behavior and performance',
|
||||
type: 'ANOMALY_DETECTION',
|
||||
accuracy: 92.1,
|
||||
lastTrained: '2024-01-15T12:00:00Z',
|
||||
status: 'TRAINING',
|
||||
predictionsToday: 67,
|
||||
correctPredictions: 62,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Incident Correlation Engine',
|
||||
description: 'Identifies related incidents and potential cascading effects',
|
||||
type: 'INCIDENT_PREDICTION',
|
||||
accuracy: 89.3,
|
||||
lastTrained: '2024-01-13T15:00:00Z',
|
||||
status: 'ACTIVE',
|
||||
predictionsToday: 23,
|
||||
correctPredictions: 20,
|
||||
},
|
||||
]);
|
||||
|
||||
setAnomalyDetections([
|
||||
{
|
||||
id: '1',
|
||||
timestamp: '2024-01-15T10:30:00Z',
|
||||
type: 'PERFORMANCE',
|
||||
severity: 'HIGH',
|
||||
description: 'Unusual CPU spike detected on database servers',
|
||||
confidence: 92.5,
|
||||
status: 'INVESTIGATING',
|
||||
affectedSystems: ['Database Cluster', 'API Gateway'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: '2024-01-15T09:45:00Z',
|
||||
type: 'SECURITY',
|
||||
severity: 'MEDIUM',
|
||||
description: 'Anomalous login patterns detected',
|
||||
confidence: 78.3,
|
||||
status: 'NEW',
|
||||
relatedIncident: 'INC-003',
|
||||
affectedSystems: ['Authentication Service'],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timestamp: '2024-01-15T08:20:00Z',
|
||||
type: 'BEHAVIORAL',
|
||||
severity: 'LOW',
|
||||
description: 'Unusual API usage patterns detected',
|
||||
confidence: 65.2,
|
||||
status: 'FALSE_POSITIVE',
|
||||
affectedSystems: ['User Service', 'API Gateway'],
|
||||
},
|
||||
]);
|
||||
|
||||
setCostAnalyses([
|
||||
{
|
||||
id: '1',
|
||||
incidentId: 'INC-001',
|
||||
incidentTitle: 'Database Connection Timeout',
|
||||
estimatedCost: 15000,
|
||||
actualCost: 12000,
|
||||
costCategory: 'DOWNTIME',
|
||||
analysisDate: '2024-01-15T11:00:00Z',
|
||||
mitigationActions: ['Automated failover', 'Cache optimization'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
incidentId: 'INC-002',
|
||||
incidentTitle: 'API Gateway Error',
|
||||
estimatedCost: 8000,
|
||||
actualCost: 6500,
|
||||
costCategory: 'RESOURCE',
|
||||
analysisDate: '2024-01-15T10:30:00Z',
|
||||
mitigationActions: ['Load balancing', 'Circuit breaker'],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
incidentId: 'INC-003',
|
||||
incidentTitle: 'Security Incident',
|
||||
estimatedCost: 25000,
|
||||
actualCost: 18000,
|
||||
costCategory: 'REPUTATION',
|
||||
analysisDate: '2024-01-15T09:15:00Z',
|
||||
mitigationActions: ['Immediate containment', 'Security audit'],
|
||||
},
|
||||
]);
|
||||
|
||||
setTrendAnalyses([
|
||||
{
|
||||
metric: 'Incident Volume',
|
||||
currentValue: 45,
|
||||
previousValue: 52,
|
||||
changePercent: -13.5,
|
||||
trend: 'DOWN',
|
||||
timeframe: 'Last 7 days',
|
||||
prediction: 42,
|
||||
},
|
||||
{
|
||||
metric: 'Resolution Time',
|
||||
currentValue: 2.3,
|
||||
previousValue: 2.8,
|
||||
changePercent: -17.9,
|
||||
trend: 'DOWN',
|
||||
timeframe: 'Last 7 days',
|
||||
prediction: 2.1,
|
||||
},
|
||||
{
|
||||
metric: 'Automation Success Rate',
|
||||
currentValue: 94.2,
|
||||
previousValue: 91.8,
|
||||
changePercent: 2.6,
|
||||
trend: 'UP',
|
||||
timeframe: 'Last 7 days',
|
||||
prediction: 95.1,
|
||||
},
|
||||
{
|
||||
metric: 'System Uptime',
|
||||
currentValue: 99.2,
|
||||
previousValue: 98.9,
|
||||
changePercent: 0.3,
|
||||
trend: 'UP',
|
||||
timeframe: 'Last 7 days',
|
||||
prediction: 99.4,
|
||||
},
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load analytics data:', error);
|
||||
setError('Failed to load analytics data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
case 'RESOLVED':
|
||||
return 'success';
|
||||
case 'TRAINING':
|
||||
case 'INVESTIGATING':
|
||||
return 'warning';
|
||||
case 'INACTIVE':
|
||||
case 'FALSE_POSITIVE':
|
||||
return 'default';
|
||||
case 'NEW':
|
||||
return 'info';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'CRITICAL':
|
||||
return 'error';
|
||||
case 'HIGH':
|
||||
return 'warning';
|
||||
case 'MEDIUM':
|
||||
return 'info';
|
||||
case 'LOW':
|
||||
return 'success';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getTrendIcon = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'UP':
|
||||
return <TrendingUpIcon color="success" />;
|
||||
case 'DOWN':
|
||||
return <TrendingDownIcon color="error" />;
|
||||
case 'STABLE':
|
||||
return <TimelineIcon color="info" />;
|
||||
default:
|
||||
return <TimelineIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
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;
|
||||
subtitle?: string;
|
||||
}> = ({ title, value, icon, color, trend, trendValue, subtitle }) => (
|
||||
<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>
|
||||
{subtitle && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
{trend && trendValue && (
|
||||
<Box display="flex" alignItems="center" mt={1}>
|
||||
{trend === 'up' ? (
|
||||
<TrendingUpIcon color="success" fontSize="small" />
|
||||
) : trend === 'down' ? (
|
||||
<TrendingDownIcon color="error" 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 Analytics Dashboard...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" action={
|
||||
<Button color="inherit" size="small" onClick={loadAnalyticsData}>
|
||||
Retry
|
||||
</Button>
|
||||
}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header */}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Analytics & Predictive Insights
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
AI-powered analytics, predictions, and intelligent insights
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<DownloadIcon />}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Export Report
|
||||
</Button>
|
||||
<IconButton onClick={loadAnalyticsData}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Analytics Status Alert */}
|
||||
<Alert
|
||||
severity={stats.predictionAccuracy > 90 ? 'success' : stats.predictionAccuracy > 80 ? 'warning' : 'error'}
|
||||
sx={{ mb: 3 }}
|
||||
icon={stats.predictionAccuracy > 90 ? <PsychologyIcon /> : <WarningIcon />}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
AI Prediction Accuracy: {stats.predictionAccuracy}%
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{stats.predictionAccuracy > 90 ? 'Excellent' : stats.predictionAccuracy > 80 ? 'Good' : 'Needs improvement'} AI model performance.
|
||||
{stats.anomalyDetections > 0 && ` ${stats.anomalyDetections} anomalies detected today.`}
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
{/* Tabs */}
|
||||
<Paper sx={{ mb: 3 }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange} indicatorColor="primary" textColor="primary">
|
||||
<Tab label="Overview" />
|
||||
<Tab label="AI Models" />
|
||||
<Tab label="Anomaly Detection" />
|
||||
<Tab label="Cost Analysis" />
|
||||
<Tab label="Trends" />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 0 && (
|
||||
<>
|
||||
{/* Stats Cards */}
|
||||
<Grid container spacing={3} mb={3}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Prediction Accuracy"
|
||||
value={`${stats.predictionAccuracy}%`}
|
||||
icon={<PsychologyIcon />}
|
||||
color="primary"
|
||||
trend="up"
|
||||
trendValue="+2% this week"
|
||||
subtitle="AI model performance"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Anomalies Detected"
|
||||
value={stats.anomalyDetections}
|
||||
icon={<WarningIcon />}
|
||||
color="warning"
|
||||
trend="down"
|
||||
trendValue="-3 from yesterday"
|
||||
subtitle="Today's detections"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Cost Savings"
|
||||
value={formatCurrency(stats.costSavings)}
|
||||
icon={<InsightsIcon />}
|
||||
color="success"
|
||||
trend="up"
|
||||
trendValue="+15% this month"
|
||||
subtitle="Through automation"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Avg Resolution Time"
|
||||
value={`${stats.avgResolutionTime}h`}
|
||||
icon={<SpeedIcon />}
|
||||
color="info"
|
||||
trend="down"
|
||||
trendValue="-0.5h this week"
|
||||
subtitle="Performance improvement"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Trend Analysis */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Key Performance Trends
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{trendAnalyses.map((trend, index) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }} key={index}>
|
||||
<Paper sx={{ p: 2, textAlign: 'center' }}>
|
||||
<Box display="flex" alignItems="center" justifyContent="center" mb={1}>
|
||||
{getTrendIcon(trend.trend)}
|
||||
<Typography variant="h6" sx={{ ml: 1 }}>
|
||||
{trend.currentValue}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{trend.metric}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color={trend.changePercent > 0 ? 'success.main' : trend.changePercent < 0 ? 'error.main' : 'textSecondary'}
|
||||
>
|
||||
{trend.changePercent > 0 ? '+' : ''}{trend.changePercent.toFixed(1)}%
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* AI Models Tab */}
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
AI Prediction Models
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Monitor and manage machine learning models for incident prediction
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3} sx={{ mt: 2 }}>
|
||||
{predictionModels.map((model) => (
|
||||
<Grid size={{ xs: 12, md: 6 }} key={model.id}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="start" mb={2}>
|
||||
<Typography variant="h6">
|
||||
{model.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={model.status}
|
||||
color={getStatusColor(model.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body2" color="textSecondary" paragraph>
|
||||
{model.description}
|
||||
</Typography>
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
Model Performance:
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" mt={1}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={model.accuracy}
|
||||
color={model.accuracy > 90 ? 'success' : model.accuracy > 80 ? 'warning' : 'error'}
|
||||
sx={{ flexGrow: 1, mr: 2 }}
|
||||
/>
|
||||
<Typography variant="body2">
|
||||
{model.accuracy}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="caption">
|
||||
Predictions Today: {model.predictionsToday}
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
Correct: {model.correctPredictions}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="caption" color="textSecondary" display="block">
|
||||
Last Trained: {formatTime(model.lastTrained)}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ mt: 2 }}
|
||||
fullWidth
|
||||
>
|
||||
View Model Details
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Anomaly Detection Tab */}
|
||||
{activeTab === 2 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Anomaly Detection
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
AI-detected anomalies and unusual patterns in system behavior
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Timestamp</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Severity</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell>Confidence</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Affected Systems</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{anomalyDetections.map((anomaly) => (
|
||||
<TableRow key={anomaly.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(anomaly.timestamp)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={anomaly.type} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={anomaly.severity}
|
||||
color={getSeverityColor(anomaly.severity) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{anomaly.description}
|
||||
</Typography>
|
||||
{anomaly.relatedIncident && (
|
||||
<Typography variant="caption" color="primary">
|
||||
Related: {anomaly.relatedIncident}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={anomaly.confidence}
|
||||
color={anomaly.confidence > 80 ? 'success' : anomaly.confidence > 60 ? 'warning' : 'error'}
|
||||
sx={{ width: 60, mr: 1 }}
|
||||
/>
|
||||
<Typography variant="caption">
|
||||
{anomaly.confidence}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={anomaly.status}
|
||||
color={getStatusColor(anomaly.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{anomaly.affectedSystems.join(', ')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Investigate
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Cost Analysis Tab */}
|
||||
{activeTab === 3 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Cost Impact Analysis
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Financial impact analysis of incidents and cost savings through automation
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Incident</TableCell>
|
||||
<TableCell>Cost Category</TableCell>
|
||||
<TableCell>Estimated Cost</TableCell>
|
||||
<TableCell>Actual Cost</TableCell>
|
||||
<TableCell>Savings</TableCell>
|
||||
<TableCell>Analysis Date</TableCell>
|
||||
<TableCell>Mitigation Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{costAnalyses.map((analysis) => (
|
||||
<TableRow key={analysis.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{analysis.incidentId}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{analysis.incidentTitle}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={analysis.costCategory} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" color="error">
|
||||
{formatCurrency(analysis.estimatedCost)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" color="warning">
|
||||
{formatCurrency(analysis.actualCost)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" color="success" fontWeight="bold">
|
||||
{formatCurrency(analysis.estimatedCost - analysis.actualCost)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(analysis.analysisDate)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{analysis.mitigationActions.join(', ')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Trends Tab */}
|
||||
{activeTab === 4 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Trend Analysis & Forecasting
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Historical trends and predictive forecasting for key metrics
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3} sx={{ mt: 2 }}>
|
||||
{trendAnalyses.map((trend, index) => (
|
||||
<Grid size={{ xs: 12, md: 6 }} key={index}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{trend.metric}
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="h4" color="primary">
|
||||
{trend.currentValue}
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center">
|
||||
{getTrendIcon(trend.trend)}
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ ml: 1 }}
|
||||
color={trend.changePercent > 0 ? 'success.main' : trend.changePercent < 0 ? 'error.main' : 'textSecondary'}
|
||||
>
|
||||
{trend.changePercent > 0 ? '+' : ''}{trend.changePercent.toFixed(1)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Previous: {trend.previousValue} ({trend.timeframe})
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Prediction: {trend.prediction}
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalyticsDashboard;
|
||||
972
etb-dashboard/src/components/Dashboard/AutomationDashboard.tsx
Normal file
972
etb-dashboard/src/components/Dashboard/AutomationDashboard.tsx
Normal file
@@ -0,0 +1,972 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Grid,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Button,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Avatar,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Tabs,
|
||||
Tab,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
PlayArrow as PlayIcon,
|
||||
Stop as StopIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
Warning as WarningIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Settings as SettingsIcon,
|
||||
Build as BuildIcon,
|
||||
Speed as SpeedIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface AutomationDashboardProps {
|
||||
onNavigateToModule: (moduleId: string) => void;
|
||||
}
|
||||
|
||||
interface AutomationStats {
|
||||
totalRunbooks: number;
|
||||
activeExecutions: number;
|
||||
successfulExecutions: number;
|
||||
failedExecutions: number;
|
||||
successRate: number;
|
||||
avgExecutionTime: number;
|
||||
maintenanceWindows: number;
|
||||
scheduledTasks: number;
|
||||
}
|
||||
|
||||
interface Runbook {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'DRAFT';
|
||||
executionCount: number;
|
||||
successRate: number;
|
||||
avgExecutionTime: number;
|
||||
lastExecuted: string;
|
||||
triggers: string[];
|
||||
steps: number;
|
||||
}
|
||||
|
||||
interface AutomationExecution {
|
||||
id: string;
|
||||
runbookName: string;
|
||||
status: 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
|
||||
startedAt: string;
|
||||
completedAt?: string;
|
||||
executionTime?: number;
|
||||
triggeredBy: string;
|
||||
incidentId?: string;
|
||||
progress: number;
|
||||
currentStep: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
interface MaintenanceWindow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
status: 'SCHEDULED' | 'ACTIVE' | 'COMPLETED' | 'CANCELLED';
|
||||
affectedServices: string[];
|
||||
suppressIncidents: boolean;
|
||||
suppressAlerts: boolean;
|
||||
}
|
||||
|
||||
interface ScheduledTask {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
schedule: string;
|
||||
nextRun: string;
|
||||
status: 'ACTIVE' | 'PAUSED' | 'ERROR';
|
||||
lastRun?: string;
|
||||
lastStatus?: 'SUCCESS' | 'FAILED';
|
||||
runbookId: string;
|
||||
}
|
||||
|
||||
const AutomationDashboard: React.FC<AutomationDashboardProps> = ({ onNavigateToModule }) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [stats, setStats] = useState<AutomationStats>({
|
||||
totalRunbooks: 0,
|
||||
activeExecutions: 0,
|
||||
successfulExecutions: 0,
|
||||
failedExecutions: 0,
|
||||
successRate: 0,
|
||||
avgExecutionTime: 0,
|
||||
maintenanceWindows: 0,
|
||||
scheduledTasks: 0,
|
||||
});
|
||||
const [runbooks, setRunbooks] = useState<Runbook[]>([]);
|
||||
const [executions, setExecutions] = useState<AutomationExecution[]>([]);
|
||||
const [maintenanceWindows, setMaintenanceWindows] = useState<MaintenanceWindow[]>([]);
|
||||
const [scheduledTasks, setScheduledTasks] = useState<ScheduledTask[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadAutomationData();
|
||||
}, []);
|
||||
|
||||
const loadAutomationData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Mock data - replace with actual API calls
|
||||
setStats({
|
||||
totalRunbooks: 45,
|
||||
activeExecutions: 3,
|
||||
successfulExecutions: 234,
|
||||
failedExecutions: 12,
|
||||
successRate: 95.1,
|
||||
avgExecutionTime: 2.3,
|
||||
maintenanceWindows: 8,
|
||||
scheduledTasks: 23,
|
||||
});
|
||||
|
||||
setRunbooks([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Database Restart Procedure',
|
||||
description: 'Automated database restart with health checks',
|
||||
category: 'Database',
|
||||
status: 'ACTIVE',
|
||||
executionCount: 45,
|
||||
successRate: 97.8,
|
||||
avgExecutionTime: 3.2,
|
||||
lastExecuted: '2024-01-15T09:30:00Z',
|
||||
triggers: ['Incident', 'Manual', 'Schedule'],
|
||||
steps: 8,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Cache Clear and Rebuild',
|
||||
description: 'Clear Redis cache and rebuild from database',
|
||||
category: 'Cache',
|
||||
status: 'ACTIVE',
|
||||
executionCount: 23,
|
||||
successRate: 91.3,
|
||||
avgExecutionTime: 1.8,
|
||||
lastExecuted: '2024-01-15T08:15:00Z',
|
||||
triggers: ['Incident', 'Manual'],
|
||||
steps: 5,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Load Balancer Failover',
|
||||
description: 'Automated failover to backup load balancer',
|
||||
category: 'Infrastructure',
|
||||
status: 'ACTIVE',
|
||||
executionCount: 12,
|
||||
successRate: 100,
|
||||
avgExecutionTime: 4.5,
|
||||
lastExecuted: '2024-01-14T16:45:00Z',
|
||||
triggers: ['Incident'],
|
||||
steps: 12,
|
||||
},
|
||||
]);
|
||||
|
||||
setExecutions([
|
||||
{
|
||||
id: '1',
|
||||
runbookName: 'Database Restart Procedure',
|
||||
status: 'RUNNING',
|
||||
startedAt: '2024-01-15T10:25:00Z',
|
||||
triggeredBy: 'John Doe',
|
||||
incidentId: 'INC-001',
|
||||
progress: 65,
|
||||
currentStep: 'Restarting database service',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
runbookName: 'Cache Clear and Rebuild',
|
||||
status: 'COMPLETED',
|
||||
startedAt: '2024-01-15T10:15:00Z',
|
||||
completedAt: '2024-01-15T10:18:00Z',
|
||||
executionTime: 3,
|
||||
triggeredBy: 'System',
|
||||
incidentId: 'INC-002',
|
||||
progress: 100,
|
||||
currentStep: 'Completed',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
runbookName: 'Load Balancer Failover',
|
||||
status: 'FAILED',
|
||||
startedAt: '2024-01-15T09:45:00Z',
|
||||
completedAt: '2024-01-15T09:50:00Z',
|
||||
executionTime: 5,
|
||||
triggeredBy: 'Jane Smith',
|
||||
incidentId: 'INC-003',
|
||||
progress: 40,
|
||||
currentStep: 'Failed at backup verification',
|
||||
errorMessage: 'Backup load balancer unreachable',
|
||||
},
|
||||
]);
|
||||
|
||||
setMaintenanceWindows([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Database Maintenance',
|
||||
description: 'Scheduled database optimization and cleanup',
|
||||
startTime: '2024-01-16T02:00:00Z',
|
||||
endTime: '2024-01-16T04:00:00Z',
|
||||
status: 'SCHEDULED',
|
||||
affectedServices: ['Database', 'API Gateway', 'User Service'],
|
||||
suppressIncidents: true,
|
||||
suppressAlerts: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Infrastructure Updates',
|
||||
description: 'Server OS updates and security patches',
|
||||
startTime: '2024-01-15T14:00:00Z',
|
||||
endTime: '2024-01-15T16:00:00Z',
|
||||
status: 'ACTIVE',
|
||||
affectedServices: ['Web Servers', 'Load Balancers'],
|
||||
suppressIncidents: false,
|
||||
suppressAlerts: true,
|
||||
},
|
||||
]);
|
||||
|
||||
setScheduledTasks([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Daily Health Check',
|
||||
description: 'Comprehensive system health check',
|
||||
schedule: '0 6 * * *',
|
||||
nextRun: '2024-01-16T06:00:00Z',
|
||||
status: 'ACTIVE',
|
||||
lastRun: '2024-01-15T06:00:00Z',
|
||||
lastStatus: 'SUCCESS',
|
||||
runbookId: 'health-check-001',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Weekly Report Generation',
|
||||
description: 'Generate weekly incident and performance reports',
|
||||
schedule: '0 9 * * 1',
|
||||
nextRun: '2024-01-22T09:00:00Z',
|
||||
status: 'ACTIVE',
|
||||
lastRun: '2024-01-15T09:00:00Z',
|
||||
lastStatus: 'SUCCESS',
|
||||
runbookId: 'report-gen-001',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Cache Cleanup',
|
||||
description: 'Clean up expired cache entries',
|
||||
schedule: '0 */4 * * *',
|
||||
nextRun: '2024-01-15T14:00:00Z',
|
||||
status: 'ERROR',
|
||||
lastRun: '2024-01-15T10:00:00Z',
|
||||
lastStatus: 'FAILED',
|
||||
runbookId: 'cache-cleanup-001',
|
||||
},
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load automation data:', error);
|
||||
setError('Failed to load automation data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'COMPLETED':
|
||||
case 'SUCCESS':
|
||||
case 'ACTIVE':
|
||||
return 'success';
|
||||
case 'RUNNING':
|
||||
case 'SCHEDULED':
|
||||
return 'info';
|
||||
case 'FAILED':
|
||||
case 'ERROR':
|
||||
case 'CANCELLED':
|
||||
return 'error';
|
||||
case 'PAUSED':
|
||||
case 'DRAFT':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getExecutionStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'RUNNING':
|
||||
return <PlayIcon color="info" />;
|
||||
case 'COMPLETED':
|
||||
return <CheckCircleIcon color="success" />;
|
||||
case 'FAILED':
|
||||
return <ErrorIcon color="error" />;
|
||||
case 'CANCELLED':
|
||||
return <StopIcon color="warning" />;
|
||||
default:
|
||||
return <PlayIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
const formatDuration = (minutes: number) => {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = Math.round(minutes % 60);
|
||||
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
||||
};
|
||||
|
||||
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;
|
||||
subtitle?: string;
|
||||
}> = ({ title, value, icon, color, trend, trendValue, subtitle }) => (
|
||||
<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>
|
||||
{subtitle && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
{trend && trendValue && (
|
||||
<Box display="flex" alignItems="center" mt={1}>
|
||||
{trend === 'up' ? (
|
||||
<TrendingUpIcon color="success" fontSize="small" />
|
||||
) : trend === 'down' ? (
|
||||
<TrendingDownIcon color="error" 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 Automation Dashboard...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" action={
|
||||
<Button color="inherit" size="small" onClick={loadAutomationData}>
|
||||
Retry
|
||||
</Button>
|
||||
}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header */}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Automation & Orchestration
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
Intelligent automation and runbook management
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<BuildIcon />}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Create Runbook
|
||||
</Button>
|
||||
<IconButton onClick={loadAutomationData}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Automation Status Alert */}
|
||||
<Alert
|
||||
severity={stats.successRate > 95 ? 'success' : stats.successRate > 85 ? 'warning' : 'error'}
|
||||
sx={{ mb: 3 }}
|
||||
icon={stats.successRate > 95 ? <CheckCircleIcon /> : <WarningIcon />}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
Automation Success Rate: {stats.successRate}%
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{stats.successRate > 95 ? 'Excellent' : stats.successRate > 85 ? 'Good' : 'Needs improvement'} automation performance.
|
||||
{stats.activeExecutions > 0 && ` ${stats.activeExecutions} executions currently running.`}
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
{/* Tabs */}
|
||||
<Paper sx={{ mb: 3 }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange} indicatorColor="primary" textColor="primary">
|
||||
<Tab label="Overview" />
|
||||
<Tab label="Runbooks" />
|
||||
<Tab label="Executions" />
|
||||
<Tab label="Maintenance" />
|
||||
<Tab label="Scheduled Tasks" />
|
||||
</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 Runbooks"
|
||||
value={stats.totalRunbooks}
|
||||
icon={<BuildIcon />}
|
||||
color="primary"
|
||||
trend="up"
|
||||
trendValue="+3 this week"
|
||||
subtitle={`${runbooks.filter(r => r.status === 'ACTIVE').length} active`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Success Rate"
|
||||
value={`${stats.successRate}%`}
|
||||
icon={<CheckCircleIcon />}
|
||||
color="success"
|
||||
trend="up"
|
||||
trendValue="+2% this month"
|
||||
subtitle="Automation reliability"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Active Executions"
|
||||
value={stats.activeExecutions}
|
||||
icon={<PlayIcon />}
|
||||
color="info"
|
||||
trend="neutral"
|
||||
trendValue="No change"
|
||||
subtitle="Currently running"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Avg Execution Time"
|
||||
value={`${stats.avgExecutionTime}m`}
|
||||
icon={<SpeedIcon />}
|
||||
color="secondary"
|
||||
trend="down"
|
||||
trendValue="-0.5m this week"
|
||||
subtitle="Performance improvement"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Current Executions */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Active Executions
|
||||
</Typography>
|
||||
{executions.filter(e => e.status === 'RUNNING').length > 0 ? (
|
||||
<List>
|
||||
{executions.filter(e => e.status === 'RUNNING').map((execution) => (
|
||||
<ListItem key={execution.id}>
|
||||
<ListItemIcon>
|
||||
{getExecutionStatusIcon(execution.status)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box display="flex" alignItems="center">
|
||||
<Typography variant="body1" sx={{ mr: 2 }}>
|
||||
{execution.runbookName}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`Step ${Math.floor(execution.progress / 10)} of ${Math.floor(100 / 10)}`}
|
||||
size="small"
|
||||
color="info"
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{execution.currentStep}
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={execution.progress}
|
||||
sx={{ mt: 1, height: 6 }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
No active executions
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Executions */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Recent Executions
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Runbook</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Triggered By</TableCell>
|
||||
<TableCell>Duration</TableCell>
|
||||
<TableCell>Incident</TableCell>
|
||||
<TableCell>Started</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{executions.slice(0, 5).map((execution) => (
|
||||
<TableRow key={execution.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{execution.runbookName}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
{getExecutionStatusIcon(execution.status)}
|
||||
<Chip
|
||||
label={execution.status}
|
||||
color={getStatusColor(execution.status) as any}
|
||||
size="small"
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{execution.triggeredBy}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{execution.executionTime ? formatDuration(execution.executionTime) : 'Running...'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{execution.incidentId || '-'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(execution.startedAt)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Runbooks Tab */}
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Runbook Library
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Manage and monitor automation runbooks
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3} sx={{ mt: 2 }}>
|
||||
{runbooks.map((runbook) => (
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }} key={runbook.id}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="start" mb={2}>
|
||||
<Typography variant="h6">
|
||||
{runbook.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={runbook.status}
|
||||
color={getStatusColor(runbook.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body2" color="textSecondary" paragraph>
|
||||
{runbook.description}
|
||||
</Typography>
|
||||
<Box mb={2}>
|
||||
<Chip label={runbook.category} size="small" sx={{ mr: 1 }} />
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{runbook.steps} steps
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="caption">
|
||||
Executions: {runbook.executionCount}
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
Success Rate: {runbook.successRate}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="caption">
|
||||
Avg Time: {formatDuration(runbook.avgExecutionTime)}
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
Last Run: {formatTime(runbook.lastExecuted)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<Typography variant="caption" color="textSecondary" display="block">
|
||||
Triggers:
|
||||
</Typography>
|
||||
{runbook.triggers.map((trigger, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={trigger}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ mr: 0.5, mt: 0.5 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<PlayIcon />}
|
||||
fullWidth
|
||||
>
|
||||
Execute
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Executions Tab */}
|
||||
{activeTab === 2 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Execution History
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Detailed execution logs and performance metrics
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Runbook</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Progress</TableCell>
|
||||
<TableCell>Triggered By</TableCell>
|
||||
<TableCell>Incident</TableCell>
|
||||
<TableCell>Duration</TableCell>
|
||||
<TableCell>Started</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{executions.map((execution) => (
|
||||
<TableRow key={execution.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{execution.runbookName}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
{getExecutionStatusIcon(execution.status)}
|
||||
<Chip
|
||||
label={execution.status}
|
||||
color={getStatusColor(execution.status) as any}
|
||||
size="small"
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={execution.progress}
|
||||
color={execution.status === 'COMPLETED' ? 'success' : execution.status === 'FAILED' ? 'error' : 'primary'}
|
||||
sx={{ width: 60, mr: 1 }}
|
||||
/>
|
||||
<Typography variant="caption">
|
||||
{execution.progress}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{execution.triggeredBy}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{execution.incidentId || '-'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{execution.executionTime ? formatDuration(execution.executionTime) : 'Running...'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(execution.startedAt)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
View Logs
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Maintenance Windows Tab */}
|
||||
{activeTab === 3 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Maintenance Windows
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Schedule and manage system maintenance windows
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3} sx={{ mt: 2 }}>
|
||||
{maintenanceWindows.map((window) => (
|
||||
<Grid size={{ xs: 12, md: 6 }} key={window.id}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="start" mb={2}>
|
||||
<Typography variant="h6">
|
||||
{window.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={window.status}
|
||||
color={getStatusColor(window.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body2" color="textSecondary" paragraph>
|
||||
{window.description}
|
||||
</Typography>
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
Schedule:
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{formatTime(window.startTime)} - {formatTime(window.endTime)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
Affected Services:
|
||||
</Typography>
|
||||
<Box sx={{ mt: 1 }}>
|
||||
{window.affectedServices.map((service, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={service}
|
||||
size="small"
|
||||
sx={{ mr: 0.5, mb: 0.5 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<FormControlLabel
|
||||
control={<Switch checked={window.suppressIncidents} />}
|
||||
label="Suppress Incidents"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={window.suppressAlerts} />}
|
||||
label="Suppress Alerts"
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Scheduled Tasks Tab */}
|
||||
{activeTab === 4 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Scheduled Tasks
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Manage automated scheduled tasks and cron jobs
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Task Name</TableCell>
|
||||
<TableCell>Schedule</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Next Run</TableCell>
|
||||
<TableCell>Last Run</TableCell>
|
||||
<TableCell>Last Status</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{scheduledTasks.map((task) => (
|
||||
<TableRow key={task.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{task.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{task.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontFamily="monospace">
|
||||
{task.schedule}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={task.status}
|
||||
color={getStatusColor(task.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(task.nextRun)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{task.lastRun ? formatTime(task.lastRun) : '-'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{task.lastStatus ? (
|
||||
<Chip
|
||||
label={task.lastStatus}
|
||||
color={getStatusColor(task.lastStatus) as any}
|
||||
size="small"
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
-
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Configure
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutomationDashboard;
|
||||
@@ -0,0 +1,882 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Grid,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Button,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Avatar,
|
||||
Tabs,
|
||||
Tab,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Stack,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Group as GroupIcon,
|
||||
Message as MessageIcon,
|
||||
PersonAdd as PersonAddIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Settings as SettingsIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
BugReport as BugReportIcon,
|
||||
Visibility as VisibilityIcon,
|
||||
Lock as LockIcon,
|
||||
Public as PublicIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface CollaborationDashboardProps {
|
||||
onNavigateToModule: (moduleId: string) => void;
|
||||
}
|
||||
|
||||
interface CollaborationStats {
|
||||
totalWarRooms: number;
|
||||
activeWarRooms: number;
|
||||
totalParticipants: number;
|
||||
activeParticipants: number;
|
||||
messagesToday: number;
|
||||
avgResponseTime: number;
|
||||
incidentsResolved: number;
|
||||
knowledgeItemsCreated: number;
|
||||
}
|
||||
|
||||
interface WarRoom {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
incidentId: string;
|
||||
incidentTitle: string;
|
||||
status: 'ACTIVE' | 'ARCHIVED' | 'CLOSED';
|
||||
privacyLevel: 'PUBLIC' | 'PRIVATE' | 'RESTRICTED';
|
||||
participants: number;
|
||||
maxParticipants: number;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
lastActivity: string;
|
||||
messageCount: number;
|
||||
integrations: string[];
|
||||
}
|
||||
|
||||
interface WarRoomParticipant {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: 'INCIDENT_COMMANDER' | 'DEPUTY' | 'COMMUNICATIONS' | 'PLANNING' | 'OPERATIONS' | 'LOGISTICS' | 'FINANCE' | 'PARTICIPANT';
|
||||
status: 'ONLINE' | 'AWAY' | 'OFFLINE';
|
||||
joinedAt: string;
|
||||
lastSeen: string;
|
||||
messageCount: number;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
interface WarRoomMessage {
|
||||
id: string;
|
||||
participantId: string;
|
||||
participantName: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
type: 'TEXT' | 'IMAGE' | 'FILE' | 'COMMAND' | 'SYSTEM';
|
||||
isEdited: boolean;
|
||||
reactions: { [emoji: string]: number };
|
||||
attachments?: string[];
|
||||
}
|
||||
|
||||
interface ChatCommand {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
usage: string;
|
||||
category: 'INCIDENT' | 'AUTOMATION' | 'INFO' | 'UTILITY';
|
||||
isEnabled: boolean;
|
||||
usageCount: number;
|
||||
}
|
||||
|
||||
const CollaborationDashboard: React.FC<CollaborationDashboardProps> = ({ onNavigateToModule }) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [stats, setStats] = useState<CollaborationStats>({
|
||||
totalWarRooms: 0,
|
||||
activeWarRooms: 0,
|
||||
totalParticipants: 0,
|
||||
activeParticipants: 0,
|
||||
messagesToday: 0,
|
||||
avgResponseTime: 0,
|
||||
incidentsResolved: 0,
|
||||
knowledgeItemsCreated: 0,
|
||||
});
|
||||
const [warRooms, setWarRooms] = useState<WarRoom[]>([]);
|
||||
const [participants, setParticipants] = useState<WarRoomParticipant[]>([]);
|
||||
const [, setMessages] = useState<WarRoomMessage[]>([]);
|
||||
const [chatCommands, setChatCommands] = useState<ChatCommand[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadCollaborationData();
|
||||
}, []);
|
||||
|
||||
const loadCollaborationData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Mock data - replace with actual API calls
|
||||
setStats({
|
||||
totalWarRooms: 23,
|
||||
activeWarRooms: 5,
|
||||
totalParticipants: 156,
|
||||
activeParticipants: 42,
|
||||
messagesToday: 1247,
|
||||
avgResponseTime: 0.8,
|
||||
incidentsResolved: 18,
|
||||
knowledgeItemsCreated: 12,
|
||||
});
|
||||
|
||||
setWarRooms([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Database Outage Response',
|
||||
description: 'Critical database connectivity issues affecting multiple services',
|
||||
incidentId: 'INC-001',
|
||||
incidentTitle: 'Database Connection Timeout',
|
||||
status: 'ACTIVE',
|
||||
privacyLevel: 'PRIVATE',
|
||||
participants: 8,
|
||||
maxParticipants: 12,
|
||||
createdBy: 'John Doe',
|
||||
createdAt: '2024-01-15T10:00:00Z',
|
||||
lastActivity: '2024-01-15T11:25:00Z',
|
||||
messageCount: 47,
|
||||
integrations: ['Slack', 'Teams', 'Jira'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'API Gateway Investigation',
|
||||
description: 'Investigation of API gateway performance degradation',
|
||||
incidentId: 'INC-002',
|
||||
incidentTitle: 'API Gateway Error',
|
||||
status: 'ACTIVE',
|
||||
privacyLevel: 'RESTRICTED',
|
||||
participants: 5,
|
||||
maxParticipants: 8,
|
||||
createdBy: 'Jane Smith',
|
||||
createdAt: '2024-01-15T09:30:00Z',
|
||||
lastActivity: '2024-01-15T11:20:00Z',
|
||||
messageCount: 32,
|
||||
integrations: ['Slack', 'PagerDuty'],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Security Incident Response',
|
||||
description: 'Response to potential security breach investigation',
|
||||
incidentId: 'INC-003',
|
||||
incidentTitle: 'Security Incident',
|
||||
status: 'ACTIVE',
|
||||
privacyLevel: 'RESTRICTED',
|
||||
participants: 6,
|
||||
maxParticipants: 10,
|
||||
createdBy: 'Mike Johnson',
|
||||
createdAt: '2024-01-15T08:45:00Z',
|
||||
lastActivity: '2024-01-15T11:15:00Z',
|
||||
messageCount: 28,
|
||||
integrations: ['Teams', 'Security Tools'],
|
||||
},
|
||||
]);
|
||||
|
||||
setParticipants([
|
||||
{
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@company.com',
|
||||
role: 'INCIDENT_COMMANDER',
|
||||
status: 'ONLINE',
|
||||
joinedAt: '2024-01-15T10:00:00Z',
|
||||
lastSeen: '2024-01-15T11:25:00Z',
|
||||
messageCount: 15,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Jane Smith',
|
||||
email: 'jane.smith@company.com',
|
||||
role: 'DEPUTY',
|
||||
status: 'ONLINE',
|
||||
joinedAt: '2024-01-15T10:05:00Z',
|
||||
lastSeen: '2024-01-15T11:24:00Z',
|
||||
messageCount: 12,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Mike Johnson',
|
||||
email: 'mike.johnson@company.com',
|
||||
role: 'OPERATIONS',
|
||||
status: 'AWAY',
|
||||
joinedAt: '2024-01-15T10:10:00Z',
|
||||
lastSeen: '2024-01-15T11:20:00Z',
|
||||
messageCount: 8,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Sarah Wilson',
|
||||
email: 'sarah.wilson@company.com',
|
||||
role: 'COMMUNICATIONS',
|
||||
status: 'ONLINE',
|
||||
joinedAt: '2024-01-15T10:15:00Z',
|
||||
lastSeen: '2024-01-15T11:25:00Z',
|
||||
messageCount: 6,
|
||||
},
|
||||
]);
|
||||
|
||||
setMessages([
|
||||
{
|
||||
id: '1',
|
||||
participantId: '1',
|
||||
participantName: 'John Doe',
|
||||
content: 'All hands on deck. We have a critical database connectivity issue affecting multiple services.',
|
||||
timestamp: '2024-01-15T10:00:00Z',
|
||||
type: 'TEXT',
|
||||
isEdited: false,
|
||||
reactions: { '👍': 5, '🔥': 2 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
participantId: '2',
|
||||
participantName: 'Jane Smith',
|
||||
content: 'I\'m checking the database cluster status now. Initial reports show connection timeouts.',
|
||||
timestamp: '2024-01-15T10:02:00Z',
|
||||
type: 'TEXT',
|
||||
isEdited: false,
|
||||
reactions: { '👀': 3 },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
participantId: '1',
|
||||
participantName: 'John Doe',
|
||||
content: '/status db-cluster-1',
|
||||
timestamp: '2024-01-15T10:03:00Z',
|
||||
type: 'COMMAND',
|
||||
isEdited: false,
|
||||
reactions: {},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
participantId: '3',
|
||||
participantName: 'Mike Johnson',
|
||||
content: 'Database cluster is showing 40% connection failures. Primary node appears to be overloaded.',
|
||||
timestamp: '2024-01-15T10:05:00Z',
|
||||
type: 'TEXT',
|
||||
isEdited: false,
|
||||
reactions: { '😰': 4, '👍': 2 },
|
||||
},
|
||||
]);
|
||||
|
||||
setChatCommands([
|
||||
{
|
||||
id: '1',
|
||||
name: 'status',
|
||||
description: 'Get status of a service or component',
|
||||
usage: '/status <service-name>',
|
||||
category: 'INFO',
|
||||
isEnabled: true,
|
||||
usageCount: 45,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'escalate',
|
||||
description: 'Escalate incident to next level',
|
||||
usage: '/escalate <reason>',
|
||||
category: 'INCIDENT',
|
||||
isEnabled: true,
|
||||
usageCount: 12,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'runbook',
|
||||
description: 'Execute a runbook or automation',
|
||||
usage: '/runbook <runbook-name>',
|
||||
category: 'AUTOMATION',
|
||||
isEnabled: true,
|
||||
usageCount: 23,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'notify',
|
||||
description: 'Send notification to stakeholders',
|
||||
usage: '/notify <message>',
|
||||
category: 'UTILITY',
|
||||
isEnabled: true,
|
||||
usageCount: 18,
|
||||
},
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load collaboration data:', error);
|
||||
setError('Failed to load collaboration data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
case 'ONLINE':
|
||||
return 'success';
|
||||
case 'AWAY':
|
||||
return 'warning';
|
||||
case 'OFFLINE':
|
||||
case 'ARCHIVED':
|
||||
return 'default';
|
||||
case 'CLOSED':
|
||||
return 'error';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getPrivacyIcon = (privacyLevel: string) => {
|
||||
switch (privacyLevel) {
|
||||
case 'PUBLIC':
|
||||
return <PublicIcon color="success" />;
|
||||
case 'PRIVATE':
|
||||
return <VisibilityIcon color="warning" />;
|
||||
case 'RESTRICTED':
|
||||
return <LockIcon color="error" />;
|
||||
default:
|
||||
return <VisibilityIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
const getRoleColor = (role: string) => {
|
||||
switch (role) {
|
||||
case 'INCIDENT_COMMANDER':
|
||||
return 'error';
|
||||
case 'DEPUTY':
|
||||
return 'warning';
|
||||
case 'COMMUNICATIONS':
|
||||
case 'PLANNING':
|
||||
case 'OPERATIONS':
|
||||
case 'LOGISTICS':
|
||||
case 'FINANCE':
|
||||
return 'info';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
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;
|
||||
subtitle?: string;
|
||||
}> = ({ title, value, icon, color, trend, trendValue, subtitle }) => (
|
||||
<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>
|
||||
{subtitle && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
{trend && trendValue && (
|
||||
<Box display="flex" alignItems="center" mt={1}>
|
||||
{trend === 'up' ? (
|
||||
<TrendingUpIcon color="success" fontSize="small" />
|
||||
) : trend === 'down' ? (
|
||||
<TrendingDownIcon color="error" 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 Collaboration Dashboard...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" action={
|
||||
<Button color="inherit" size="small" onClick={loadCollaborationData}>
|
||||
Retry
|
||||
</Button>
|
||||
}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header */}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
War Rooms & Collaboration
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
Real-time incident collaboration and communication
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<GroupIcon />}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Create War Room
|
||||
</Button>
|
||||
<IconButton onClick={loadCollaborationData}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Collaboration Status Alert */}
|
||||
<Alert
|
||||
severity={stats.activeWarRooms > 0 ? 'info' : 'success'}
|
||||
sx={{ mb: 3 }}
|
||||
icon={stats.activeWarRooms > 0 ? <GroupIcon /> : <CheckCircleIcon />}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
Active War Rooms: {stats.activeWarRooms}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{stats.activeWarRooms > 0 ? 'Currently managing incidents with active collaboration rooms.' : 'No active war rooms. All incidents resolved or in normal operations.'}
|
||||
{stats.activeParticipants > 0 && ` ${stats.activeParticipants} participants currently online.`}
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
{/* Tabs */}
|
||||
<Paper sx={{ mb: 3 }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange} indicatorColor="primary" textColor="primary">
|
||||
<Tab label="Overview" />
|
||||
<Tab label="War Rooms" />
|
||||
<Tab label="Participants" />
|
||||
<Tab label="Chat Commands" />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 0 && (
|
||||
<>
|
||||
{/* Stats Cards */}
|
||||
<Grid container spacing={3} mb={3}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Active War Rooms"
|
||||
value={stats.activeWarRooms}
|
||||
icon={<GroupIcon />}
|
||||
color="primary"
|
||||
trend="up"
|
||||
trendValue="+2 this week"
|
||||
subtitle={`${stats.totalWarRooms} total rooms`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Active Participants"
|
||||
value={stats.activeParticipants}
|
||||
icon={<PersonAddIcon />}
|
||||
color="success"
|
||||
trend="up"
|
||||
trendValue="+8 today"
|
||||
subtitle={`${stats.totalParticipants} total users`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Messages Today"
|
||||
value={stats.messagesToday}
|
||||
icon={<MessageIcon />}
|
||||
color="info"
|
||||
trend="up"
|
||||
trendValue="+156 from yesterday"
|
||||
subtitle="Real-time collaboration"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Incidents Resolved"
|
||||
value={stats.incidentsResolved}
|
||||
icon={<CheckCircleIcon />}
|
||||
color="success"
|
||||
trend="up"
|
||||
trendValue="+3 this week"
|
||||
subtitle="Through collaboration"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Active War Rooms */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Active War Rooms
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{warRooms.filter(room => room.status === 'ACTIVE').map((room) => (
|
||||
<Grid size={{ xs: 12, md: 4 }} key={room.id}>
|
||||
<Paper sx={{ p: 2, border: '1px solid', borderColor: 'primary.main' }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="start" mb={2}>
|
||||
<Typography variant="h6">
|
||||
{room.name}
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center">
|
||||
{getPrivacyIcon(room.privacyLevel)}
|
||||
<Chip
|
||||
label={room.status}
|
||||
color={getStatusColor(room.status) as any}
|
||||
size="small"
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="body2" color="textSecondary" paragraph>
|
||||
{room.description}
|
||||
</Typography>
|
||||
<Box display="flex" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="caption">
|
||||
<BugReportIcon fontSize="small" sx={{ mr: 0.5, verticalAlign: 'middle' }} />
|
||||
{room.incidentId}
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
<GroupIcon fontSize="small" sx={{ mr: 0.5, verticalAlign: 'middle' }} />
|
||||
{room.participants}/{room.maxParticipants}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="caption">
|
||||
<MessageIcon fontSize="small" sx={{ mr: 0.5, verticalAlign: 'middle' }} />
|
||||
{room.messageCount} messages
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
Last activity: {formatTime(room.lastActivity)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="textSecondary" display="block">
|
||||
Integrations:
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={0.5} sx={{ mt: 0.5 }}>
|
||||
{room.integrations.map((integration, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={integration}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* War Rooms Tab */}
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
War Room Management
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Manage incident collaboration rooms and communications
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>War Room</TableCell>
|
||||
<TableCell>Incident</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Privacy</TableCell>
|
||||
<TableCell>Participants</TableCell>
|
||||
<TableCell>Messages</TableCell>
|
||||
<TableCell>Last Activity</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{warRooms.map((room) => (
|
||||
<TableRow key={room.id}>
|
||||
<TableCell>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{room.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{room.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{room.incidentId}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{room.incidentTitle}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={room.status}
|
||||
color={getStatusColor(room.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
{getPrivacyIcon(room.privacyLevel)}
|
||||
<Typography variant="body2" sx={{ ml: 1 }}>
|
||||
{room.privacyLevel}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{room.participants}/{room.maxParticipants}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{room.messageCount}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(room.lastActivity)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Join
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Participants Tab */}
|
||||
{activeTab === 2 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
War Room Participants
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Monitor participant activity and engagement
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Participant</TableCell>
|
||||
<TableCell>Role</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Messages</TableCell>
|
||||
<TableCell>Joined</TableCell>
|
||||
<TableCell>Last Seen</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{participants.map((participant) => (
|
||||
<TableRow key={participant.id}>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Avatar sx={{ mr: 2 }}>
|
||||
{participant.name.split(' ').map(n => n[0]).join('')}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{participant.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{participant.email}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={participant.role.replace('_', ' ')}
|
||||
color={getRoleColor(participant.role) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: participant.status === 'ONLINE' ? 'success.main' :
|
||||
participant.status === 'AWAY' ? 'warning.main' : 'grey.400',
|
||||
mr: 1
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2">
|
||||
{participant.status}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{participant.messageCount}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(participant.joinedAt)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(participant.lastSeen)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Message
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Chat Commands Tab */}
|
||||
{activeTab === 3 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Chat Commands & Automation
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Manage chat commands and automated responses
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3} sx={{ mt: 2 }}>
|
||||
{chatCommands.map((command) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={command.id}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="start" mb={2}>
|
||||
<Typography variant="h6">
|
||||
/{command.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={command.category}
|
||||
size="small"
|
||||
color="info"
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body2" color="textSecondary" paragraph>
|
||||
{command.description}
|
||||
</Typography>
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
Usage:
|
||||
</Typography>
|
||||
<Typography variant="body2" fontFamily="monospace" sx={{ bgcolor: 'grey.100', p: 1, borderRadius: 1 }}>
|
||||
{command.usage}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Typography variant="caption">
|
||||
Used {command.usageCount} times
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={command.isEnabled} />}
|
||||
label="Enabled"
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
fullWidth
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollaborationDashboard;
|
||||
935
etb-dashboard/src/components/Dashboard/ComplianceDashboard.tsx
Normal file
935
etb-dashboard/src/components/Dashboard/ComplianceDashboard.tsx
Normal file
@@ -0,0 +1,935 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Grid,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Button,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Avatar,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Gavel as GavelIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Settings as SettingsIcon,
|
||||
Policy as PolicyIcon,
|
||||
VerifiedUser as VerifiedUserIcon,
|
||||
Warning as WarningIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface ComplianceDashboardProps {
|
||||
onNavigateToModule: (moduleId: string) => void;
|
||||
}
|
||||
|
||||
interface ComplianceStats {
|
||||
totalPolicies: number;
|
||||
activePolicies: number;
|
||||
complianceScore: number;
|
||||
violationsFound: number;
|
||||
violationsResolved: number;
|
||||
auditScore: number;
|
||||
lastAuditDate: string;
|
||||
nextAuditDate: string;
|
||||
}
|
||||
|
||||
interface CompliancePolicy {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'SECURITY' | 'DATA_PROTECTION' | 'INCIDENT_RESPONSE' | 'ACCESS_CONTROL' | 'AUDIT';
|
||||
framework: 'SOX' | 'HIPAA' | 'GDPR' | 'PCI-DSS' | 'ISO27001' | 'NIST';
|
||||
status: 'ACTIVE' | 'DRAFT' | 'REVIEW' | 'ARCHIVED';
|
||||
lastUpdated: string;
|
||||
owner: string;
|
||||
complianceLevel: number;
|
||||
violations: number;
|
||||
}
|
||||
|
||||
interface ComplianceViolation {
|
||||
id: string;
|
||||
policyId: string;
|
||||
policyName: string;
|
||||
description: string;
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
status: 'OPEN' | 'INVESTIGATING' | 'RESOLVED' | 'ACCEPTED_RISK';
|
||||
discoveredAt: string;
|
||||
resolvedAt?: string;
|
||||
assignedTo: string;
|
||||
framework: string;
|
||||
remediationSteps: string[];
|
||||
}
|
||||
|
||||
interface AuditFinding {
|
||||
id: string;
|
||||
auditId: string;
|
||||
auditName: string;
|
||||
finding: string;
|
||||
category: 'COMPLIANCE' | 'SECURITY' | 'PROCESS' | 'TECHNICAL';
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
status: 'OPEN' | 'IN_PROGRESS' | 'RESOLVED' | 'ACCEPTED';
|
||||
discoveredAt: string;
|
||||
dueDate: string;
|
||||
assignedTo: string;
|
||||
framework: string;
|
||||
}
|
||||
|
||||
interface ComplianceReport {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'MONTHLY' | 'QUARTERLY' | 'ANNUAL' | 'AUDIT';
|
||||
framework: string;
|
||||
generatedAt: string;
|
||||
score: number;
|
||||
status: 'GENERATED' | 'REVIEW' | 'APPROVED' | 'PUBLISHED';
|
||||
findings: number;
|
||||
violations: number;
|
||||
recommendations: number;
|
||||
}
|
||||
|
||||
const ComplianceDashboard: React.FC<ComplianceDashboardProps> = ({ onNavigateToModule }) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [stats, setStats] = useState<ComplianceStats>({
|
||||
totalPolicies: 0,
|
||||
activePolicies: 0,
|
||||
complianceScore: 0,
|
||||
violationsFound: 0,
|
||||
violationsResolved: 0,
|
||||
auditScore: 0,
|
||||
lastAuditDate: '',
|
||||
nextAuditDate: '',
|
||||
});
|
||||
const [policies, setPolicies] = useState<CompliancePolicy[]>([]);
|
||||
const [violations, setViolations] = useState<ComplianceViolation[]>([]);
|
||||
const [auditFindings, setAuditFindings] = useState<AuditFinding[]>([]);
|
||||
const [reports, setReports] = useState<ComplianceReport[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadComplianceData();
|
||||
}, []);
|
||||
|
||||
const loadComplianceData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Mock data - replace with actual API calls
|
||||
setStats({
|
||||
totalPolicies: 45,
|
||||
activePolicies: 42,
|
||||
complianceScore: 94,
|
||||
violationsFound: 8,
|
||||
violationsResolved: 23,
|
||||
auditScore: 87,
|
||||
lastAuditDate: '2024-01-01T00:00:00Z',
|
||||
nextAuditDate: '2024-04-01T00:00:00Z',
|
||||
});
|
||||
|
||||
setPolicies([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Data Classification and Handling',
|
||||
description: 'Policy for classifying and handling sensitive data according to GDPR requirements',
|
||||
category: 'DATA_PROTECTION',
|
||||
framework: 'GDPR',
|
||||
status: 'ACTIVE',
|
||||
lastUpdated: '2024-01-15T10:30:00Z',
|
||||
owner: 'Data Protection Officer',
|
||||
complianceLevel: 95,
|
||||
violations: 2,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Incident Response Procedures',
|
||||
description: 'Standardized procedures for responding to security incidents',
|
||||
category: 'INCIDENT_RESPONSE',
|
||||
framework: 'ISO27001',
|
||||
status: 'ACTIVE',
|
||||
lastUpdated: '2024-01-14T15:20:00Z',
|
||||
owner: 'Security Team',
|
||||
complianceLevel: 89,
|
||||
violations: 1,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Access Control Management',
|
||||
description: 'Controls for managing user access to systems and data',
|
||||
category: 'ACCESS_CONTROL',
|
||||
framework: 'SOX',
|
||||
status: 'ACTIVE',
|
||||
lastUpdated: '2024-01-13T09:15:00Z',
|
||||
owner: 'IT Security',
|
||||
complianceLevel: 92,
|
||||
violations: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
setViolations([
|
||||
{
|
||||
id: '1',
|
||||
policyId: '1',
|
||||
policyName: 'Data Classification and Handling',
|
||||
description: 'Personal data stored without proper encryption',
|
||||
severity: 'HIGH',
|
||||
status: 'INVESTIGATING',
|
||||
discoveredAt: '2024-01-15T10:00:00Z',
|
||||
assignedTo: 'Data Protection Officer',
|
||||
framework: 'GDPR',
|
||||
remediationSteps: ['Encrypt personal data', 'Review data storage practices', 'Update classification procedures'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
policyId: '2',
|
||||
policyName: 'Incident Response Procedures',
|
||||
description: 'Incident response time exceeded SLA requirements',
|
||||
severity: 'MEDIUM',
|
||||
status: 'OPEN',
|
||||
discoveredAt: '2024-01-14T16:30:00Z',
|
||||
assignedTo: 'Incident Response Team',
|
||||
framework: 'ISO27001',
|
||||
remediationSteps: ['Review response procedures', 'Update escalation matrix', 'Conduct team training'],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
policyId: '3',
|
||||
policyName: 'Access Control Management',
|
||||
description: 'Orphaned user accounts found in active directory',
|
||||
severity: 'LOW',
|
||||
status: 'RESOLVED',
|
||||
discoveredAt: '2024-01-13T14:20:00Z',
|
||||
resolvedAt: '2024-01-14T10:15:00Z',
|
||||
assignedTo: 'IT Administrator',
|
||||
framework: 'SOX',
|
||||
remediationSteps: ['Remove orphaned accounts', 'Implement automated cleanup', 'Review access provisioning'],
|
||||
},
|
||||
]);
|
||||
|
||||
setAuditFindings([
|
||||
{
|
||||
id: '1',
|
||||
auditId: 'AUD-2024-001',
|
||||
auditName: 'Q1 2024 Compliance Audit',
|
||||
finding: 'Insufficient logging of data access events',
|
||||
category: 'COMPLIANCE',
|
||||
severity: 'HIGH',
|
||||
status: 'IN_PROGRESS',
|
||||
discoveredAt: '2024-01-10T00:00:00Z',
|
||||
dueDate: '2024-02-15T00:00:00Z',
|
||||
assignedTo: 'Security Team',
|
||||
framework: 'GDPR',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
auditId: 'AUD-2024-001',
|
||||
auditName: 'Q1 2024 Compliance Audit',
|
||||
finding: 'Missing documentation for incident response procedures',
|
||||
category: 'PROCESS',
|
||||
severity: 'MEDIUM',
|
||||
status: 'OPEN',
|
||||
discoveredAt: '2024-01-10T00:00:00Z',
|
||||
dueDate: '2024-02-28T00:00:00Z',
|
||||
assignedTo: 'Process Owner',
|
||||
framework: 'ISO27001',
|
||||
},
|
||||
]);
|
||||
|
||||
setReports([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Q1 2024 Compliance Report',
|
||||
type: 'QUARTERLY',
|
||||
framework: 'GDPR',
|
||||
generatedAt: '2024-01-15T12:00:00Z',
|
||||
score: 94,
|
||||
status: 'GENERATED',
|
||||
findings: 5,
|
||||
violations: 2,
|
||||
recommendations: 8,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Annual Security Audit Report',
|
||||
type: 'ANNUAL',
|
||||
framework: 'ISO27001',
|
||||
generatedAt: '2024-01-01T00:00:00Z',
|
||||
score: 87,
|
||||
status: 'APPROVED',
|
||||
findings: 12,
|
||||
violations: 3,
|
||||
recommendations: 15,
|
||||
},
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load compliance data:', error);
|
||||
setError('Failed to load compliance data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
case 'RESOLVED':
|
||||
case 'APPROVED':
|
||||
case 'PUBLISHED':
|
||||
return 'success';
|
||||
case 'DRAFT':
|
||||
case 'IN_PROGRESS':
|
||||
case 'REVIEW':
|
||||
return 'warning';
|
||||
case 'OPEN':
|
||||
case 'GENERATED':
|
||||
return 'info';
|
||||
case 'ARCHIVED':
|
||||
case 'ACCEPTED_RISK':
|
||||
case 'ACCEPTED':
|
||||
return 'default';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'CRITICAL':
|
||||
return 'error';
|
||||
case 'HIGH':
|
||||
return 'warning';
|
||||
case 'MEDIUM':
|
||||
return 'info';
|
||||
case 'LOW':
|
||||
return 'success';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getFrameworkColor = (framework: string) => {
|
||||
switch (framework) {
|
||||
case 'SOX':
|
||||
return 'primary';
|
||||
case 'HIPAA':
|
||||
return 'secondary';
|
||||
case 'GDPR':
|
||||
return 'success';
|
||||
case 'PCI-DSS':
|
||||
return 'warning';
|
||||
case 'ISO27001':
|
||||
return 'info';
|
||||
case 'NIST':
|
||||
return 'error';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
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;
|
||||
subtitle?: string;
|
||||
}> = ({ title, value, icon, color, trend, trendValue, subtitle }) => (
|
||||
<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>
|
||||
{subtitle && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
{trend && trendValue && (
|
||||
<Box display="flex" alignItems="center" mt={1}>
|
||||
{trend === 'up' ? (
|
||||
<TrendingUpIcon color="success" fontSize="small" />
|
||||
) : trend === 'down' ? (
|
||||
<TrendingDownIcon color="error" 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 Compliance Dashboard...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" action={
|
||||
<Button color="inherit" size="small" onClick={loadComplianceData}>
|
||||
Retry
|
||||
</Button>
|
||||
}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header */}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Compliance & Governance
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
Regulatory compliance management and governance framework
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<PolicyIcon />}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Create Policy
|
||||
</Button>
|
||||
<IconButton onClick={loadComplianceData}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Compliance Status Alert */}
|
||||
<Alert
|
||||
severity={stats.complianceScore > 90 ? 'success' : stats.complianceScore > 75 ? 'warning' : 'error'}
|
||||
sx={{ mb: 3 }}
|
||||
icon={stats.complianceScore > 90 ? <VerifiedUserIcon /> : <WarningIcon />}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
Overall Compliance Score: {stats.complianceScore}%
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{stats.complianceScore > 90 ? 'Excellent compliance posture' : stats.complianceScore > 75 ? 'Good compliance with room for improvement' : 'Compliance issues require immediate attention'}.
|
||||
{stats.violationsFound > 0 && ` ${stats.violationsFound} active violations need attention.`}
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
{/* Tabs */}
|
||||
<Paper sx={{ mb: 3 }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange} indicatorColor="primary" textColor="primary">
|
||||
<Tab label="Overview" />
|
||||
<Tab label="Policies" />
|
||||
<Tab label="Violations" />
|
||||
<Tab label="Audit Findings" />
|
||||
<Tab label="Reports" />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 0 && (
|
||||
<>
|
||||
{/* Stats Cards */}
|
||||
<Grid container spacing={3} mb={3}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Compliance Score"
|
||||
value={`${stats.complianceScore}%`}
|
||||
icon={<AssessmentIcon />}
|
||||
color="primary"
|
||||
trend="up"
|
||||
trendValue="+3% this quarter"
|
||||
subtitle="Overall compliance"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Active Policies"
|
||||
value={stats.activePolicies}
|
||||
icon={<PolicyIcon />}
|
||||
color="success"
|
||||
trend="up"
|
||||
trendValue="+2 this month"
|
||||
subtitle={`${stats.totalPolicies} total policies`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Violations Found"
|
||||
value={stats.violationsFound}
|
||||
icon={<WarningIcon />}
|
||||
color="warning"
|
||||
trend="down"
|
||||
trendValue="-2 this week"
|
||||
subtitle={`${stats.violationsResolved} resolved`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Audit Score"
|
||||
value={`${stats.auditScore}%`}
|
||||
icon={<GavelIcon />}
|
||||
color="info"
|
||||
trend="up"
|
||||
trendValue="+5% this year"
|
||||
subtitle="Last audit performance"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Compliance Frameworks */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Compliance Framework Status
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{['SOX', 'HIPAA', 'GDPR', 'PCI-DSS', 'ISO27001'].map((framework) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 2.4 }} key={framework}>
|
||||
<Paper sx={{ p: 2, textAlign: 'center' }}>
|
||||
<Typography variant="h6" color="primary">
|
||||
{framework}
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ my: 1 }}>
|
||||
{Math.floor(Math.random() * 20) + 80}%
|
||||
</Typography>
|
||||
<Chip
|
||||
label="Compliant"
|
||||
color="success"
|
||||
size="small"
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Policies Tab */}
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Compliance Policies
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Manage compliance policies and procedures
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Policy Name</TableCell>
|
||||
<TableCell>Category</TableCell>
|
||||
<TableCell>Framework</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Compliance Level</TableCell>
|
||||
<TableCell>Violations</TableCell>
|
||||
<TableCell>Owner</TableCell>
|
||||
<TableCell>Last Updated</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{policies.map((policy) => (
|
||||
<TableRow key={policy.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{policy.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{policy.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={policy.category.replace('_', ' ')} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={policy.framework}
|
||||
color={getFrameworkColor(policy.framework) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={policy.status}
|
||||
color={getStatusColor(policy.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={policy.complianceLevel}
|
||||
color={policy.complianceLevel > 90 ? 'success' : policy.complianceLevel > 75 ? 'warning' : 'error'}
|
||||
sx={{ width: 60, mr: 1 }}
|
||||
/>
|
||||
<Typography variant="caption">
|
||||
{policy.complianceLevel}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{policy.violations}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{policy.owner}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(policy.lastUpdated)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
View
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Violations Tab */}
|
||||
{activeTab === 2 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Compliance Violations
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Track and manage compliance violations and remediation
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Violation</TableCell>
|
||||
<TableCell>Policy</TableCell>
|
||||
<TableCell>Severity</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Framework</TableCell>
|
||||
<TableCell>Assigned To</TableCell>
|
||||
<TableCell>Discovered</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{violations.map((violation) => (
|
||||
<TableRow key={violation.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{violation.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{violation.policyName}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={violation.severity}
|
||||
color={getSeverityColor(violation.severity) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={violation.status}
|
||||
color={getStatusColor(violation.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={violation.framework}
|
||||
color={getFrameworkColor(violation.framework) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{violation.assignedTo}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(violation.discoveredAt)}
|
||||
</Typography>
|
||||
{violation.resolvedAt && (
|
||||
<Typography variant="caption" color="success" display="block">
|
||||
Resolved: {formatTime(violation.resolvedAt)}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
View Details
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Audit Findings Tab */}
|
||||
{activeTab === 3 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Audit Findings
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Track audit findings and remediation progress
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Finding</TableCell>
|
||||
<TableCell>Audit</TableCell>
|
||||
<TableCell>Category</TableCell>
|
||||
<TableCell>Severity</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Framework</TableCell>
|
||||
<TableCell>Assigned To</TableCell>
|
||||
<TableCell>Due Date</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{auditFindings.map((finding) => (
|
||||
<TableRow key={finding.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{finding.finding}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{finding.auditName}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{finding.auditId}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={finding.category} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={finding.severity}
|
||||
color={getSeverityColor(finding.severity) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={finding.status}
|
||||
color={getStatusColor(finding.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={finding.framework}
|
||||
color={getFrameworkColor(finding.framework) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{finding.assignedTo}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(finding.dueDate)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Update Status
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Reports Tab */}
|
||||
{activeTab === 4 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Compliance Reports
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Generate and manage compliance reports
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Report Name</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Framework</TableCell>
|
||||
<TableCell>Score</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Findings</TableCell>
|
||||
<TableCell>Violations</TableCell>
|
||||
<TableCell>Generated</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reports.map((report) => (
|
||||
<TableRow key={report.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{report.name}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={report.type} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={report.framework}
|
||||
color={getFrameworkColor(report.framework) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={report.score}
|
||||
color={report.score > 90 ? 'success' : report.score > 75 ? 'warning' : 'error'}
|
||||
sx={{ width: 60, mr: 1 }}
|
||||
/>
|
||||
<Typography variant="caption">
|
||||
{report.score}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={report.status}
|
||||
color={getStatusColor(report.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{report.findings}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{report.violations}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(report.generatedAt)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
View Report
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComplianceDashboard;
|
||||
524
etb-dashboard/src/components/Dashboard/DashboardLayout.tsx
Normal file
524
etb-dashboard/src/components/Dashboard/DashboardLayout.tsx
Normal file
@@ -0,0 +1,524 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Drawer,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
List,
|
||||
Typography,
|
||||
Divider,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Avatar,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Badge,
|
||||
Chip,
|
||||
Paper,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Menu as MenuIcon,
|
||||
Dashboard as DashboardIcon,
|
||||
Security as SecurityIcon,
|
||||
BugReport as IncidentIcon,
|
||||
Schedule as SLIcon,
|
||||
Monitor as MonitorIcon,
|
||||
Group as CollaborationIcon,
|
||||
AutoFixHigh as AutomationIcon,
|
||||
Assessment as AnalyticsIcon,
|
||||
School as KnowledgeIcon,
|
||||
Gavel as ComplianceIcon,
|
||||
AdminPanelSettings as UserManagementIcon,
|
||||
Notifications as NotificationsIcon,
|
||||
AccountCircle as ProfileIcon,
|
||||
Settings as SettingsIcon,
|
||||
Logout as LogoutIcon,
|
||||
ChevronLeft as ChevronLeftIcon,
|
||||
Warning as WarningIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
Info as InfoIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { NavigationItem } from '../../types';
|
||||
|
||||
const drawerWidth = 240;
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children }) => {
|
||||
const { user, logout, isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [systemStatus, setSystemStatus] = useState<any>(null);
|
||||
const [notifications, setNotifications] = useState<any[]>([]);
|
||||
|
||||
// Navigation items based on user permissions
|
||||
const navigationItems: NavigationItem[] = [
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: 'Dashboard',
|
||||
icon: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
id: 'incidents',
|
||||
label: 'Incidents',
|
||||
icon: 'BugReport',
|
||||
path: '/incidents',
|
||||
permissions: ['view_incident'],
|
||||
},
|
||||
{
|
||||
id: 'monitoring',
|
||||
label: 'Monitoring',
|
||||
icon: 'Monitor',
|
||||
path: '/monitoring',
|
||||
permissions: ['view_monitoringdashboard', 'view_alert', 'view_healthcheck'],
|
||||
},
|
||||
{
|
||||
id: 'sla',
|
||||
label: 'SLA & On-Call',
|
||||
icon: 'Schedule',
|
||||
path: '/sla',
|
||||
permissions: ['view_sladefinition', 'view_slainstance', 'view_oncallassignment'],
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
label: 'Security',
|
||||
icon: 'Security',
|
||||
path: '/security',
|
||||
permissions: ['view_user', 'view_role', 'view_auditlog', 'view_securityevent'],
|
||||
clearance_level: 2,
|
||||
},
|
||||
{
|
||||
id: 'automation',
|
||||
label: 'Automation',
|
||||
icon: 'AutoFixHigh',
|
||||
path: '/automation',
|
||||
permissions: ['view_runbook', 'view_autoremediation', 'view_integration'],
|
||||
},
|
||||
{
|
||||
id: 'collaboration',
|
||||
label: 'War Rooms',
|
||||
icon: 'Group',
|
||||
path: '/collaboration',
|
||||
permissions: ['view_warroom', 'view_warroommessage', 'view_conferencebridge'],
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
label: 'Analytics',
|
||||
icon: 'Assessment',
|
||||
path: '/analytics',
|
||||
permissions: ['view_kpimetric', 'view_anomalydetection', 'view_dashboardconfiguration'],
|
||||
},
|
||||
{
|
||||
id: 'knowledge',
|
||||
label: 'Knowledge',
|
||||
icon: 'School',
|
||||
path: '/knowledge',
|
||||
permissions: ['view_knowledgebasearticle', 'view_postmortem', 'view_learningpattern'],
|
||||
},
|
||||
{
|
||||
id: 'compliance',
|
||||
label: 'Compliance',
|
||||
icon: 'Gavel',
|
||||
path: '/compliance',
|
||||
permissions: ['view_compliancereport', 'view_regulatoryframework', 'view_legalhold'],
|
||||
clearance_level: 3,
|
||||
},
|
||||
{
|
||||
id: 'user-management',
|
||||
label: 'User Management',
|
||||
icon: 'AdminPanelSettings',
|
||||
path: '/user-management',
|
||||
permissions: ['view_user', 'change_user'],
|
||||
clearance_level: 5,
|
||||
},
|
||||
];
|
||||
|
||||
const iconMap: { [key: string]: React.ComponentType } = {
|
||||
Dashboard: DashboardIcon,
|
||||
BugReport: IncidentIcon,
|
||||
Monitor: MonitorIcon,
|
||||
Schedule: SLIcon,
|
||||
Security: SecurityIcon,
|
||||
AutoFixHigh: AutomationIcon,
|
||||
Group: CollaborationIcon,
|
||||
Assessment: AnalyticsIcon,
|
||||
School: KnowledgeIcon,
|
||||
Gavel: ComplianceIcon,
|
||||
AdminPanelSettings: UserManagementIcon,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
navigate('/login');
|
||||
}
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load system status and notifications
|
||||
loadSystemStatus();
|
||||
loadNotifications();
|
||||
}, []);
|
||||
|
||||
const loadSystemStatus = async () => {
|
||||
try {
|
||||
// This would be an API call to get system status
|
||||
setSystemStatus({
|
||||
status: 'OPERATIONAL',
|
||||
message: 'All systems operational',
|
||||
last_updated: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load system status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadNotifications = async () => {
|
||||
try {
|
||||
// This would be an API call to get notifications
|
||||
setNotifications([
|
||||
{
|
||||
id: '1',
|
||||
type: 'warning',
|
||||
title: 'High CPU Usage',
|
||||
message: 'Server CPU usage is above 80%',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'info',
|
||||
title: 'Scheduled Maintenance',
|
||||
message: 'System maintenance scheduled for tonight',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Failed to load notifications:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setMobileOpen(!mobileOpen);
|
||||
};
|
||||
|
||||
const handleProfileMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleProfileMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
navigate('/login');
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigation = (path: string) => {
|
||||
navigate(path);
|
||||
setMobileOpen(false);
|
||||
};
|
||||
|
||||
const canAccessItem = (item: NavigationItem): boolean => {
|
||||
if (!user) {
|
||||
console.log('No user found, denying access to:', item.label);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Checking access for:', item.label, {
|
||||
user,
|
||||
item,
|
||||
userRoles: user.roles,
|
||||
userPermissions: user.roles?.flatMap(role => role.permissions.map(perm => perm.codename)) || [],
|
||||
clearanceLevel: user.clearance_level?.level,
|
||||
isSuperuser: user.is_superuser,
|
||||
isStaff: user.is_staff
|
||||
});
|
||||
|
||||
// Superusers and staff bypass all permission checks
|
||||
if (user.is_superuser || user.is_staff) {
|
||||
console.log(`Access granted for ${item.label}: superuser/staff bypass`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check clearance level
|
||||
if (item.clearance_level && user.clearance_level) {
|
||||
if (user.clearance_level.level < item.clearance_level) {
|
||||
console.log(`Access denied for ${item.label}: insufficient clearance level (${user.clearance_level.level} < ${item.clearance_level})`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if (item.permissions && item.permissions.length > 0) {
|
||||
// For superusers, we assume they have all permissions
|
||||
if (user.is_superuser) {
|
||||
console.log(`Permission granted for ${item.label}: superuser has all permissions`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const userPermissions = user.roles?.flatMap(role =>
|
||||
role.permissions.map(perm => perm.codename)
|
||||
) || [];
|
||||
const hasPermission = item.permissions.some(permission =>
|
||||
userPermissions.includes(permission)
|
||||
);
|
||||
console.log(`Permission check for ${item.label}:`, {
|
||||
required: item.permissions,
|
||||
userHas: userPermissions,
|
||||
hasPermission
|
||||
});
|
||||
return hasPermission;
|
||||
}
|
||||
|
||||
console.log(`Access granted for ${item.label}: no restrictions`);
|
||||
return true;
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'OPERATIONAL': return 'success';
|
||||
case 'DEGRADED': return 'warning';
|
||||
case 'PARTIAL_OUTAGE': return 'error';
|
||||
case 'MAJOR_OUTAGE': return 'error';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'OPERATIONAL': return <CheckCircleIcon />;
|
||||
case 'DEGRADED': return <WarningIcon />;
|
||||
case 'PARTIAL_OUTAGE': return <ErrorIcon />;
|
||||
case 'MAJOR_OUTAGE': return <ErrorIcon />;
|
||||
default: return <InfoIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
const drawer = (
|
||||
<Box>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
ETB Dashboard
|
||||
</Typography>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="close drawer"
|
||||
edge="end"
|
||||
onClick={handleDrawerToggle}
|
||||
sx={{ display: { sm: 'none' } }}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
|
||||
{/* System Status */}
|
||||
{systemStatus && (
|
||||
<Paper sx={{ m: 2, p: 2 }}>
|
||||
<Box display="flex" alignItems="center" sx={{ mb: 1 }}>
|
||||
{getStatusIcon(systemStatus.status)}
|
||||
<Typography variant="subtitle2" sx={{ ml: 1 }}>
|
||||
System Status
|
||||
</Typography>
|
||||
</Box>
|
||||
<Chip
|
||||
label={systemStatus.status}
|
||||
color={getStatusColor(systemStatus.status) as any}
|
||||
size="small"
|
||||
sx={{ mb: 1 }}
|
||||
/>
|
||||
<Typography variant="caption" display="block" color="text.secondary">
|
||||
{systemStatus.message}
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Navigation */}
|
||||
<List>
|
||||
{navigationItems
|
||||
.filter(canAccessItem)
|
||||
.map((item) => {
|
||||
const IconComponent = iconMap[item.icon];
|
||||
const isActive = location.pathname === item.path;
|
||||
|
||||
return (
|
||||
<ListItem key={item.id} disablePadding>
|
||||
<ListItemButton
|
||||
selected={isActive}
|
||||
onClick={() => handleNavigation(item.path)}
|
||||
sx={{
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ color: isActive ? 'inherit' : undefined }}>
|
||||
{IconComponent && <IconComponent />}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={item.label} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<AppBar
|
||||
position="fixed"
|
||||
sx={{
|
||||
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||
ml: { sm: `${drawerWidth}px` },
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
edge="start"
|
||||
onClick={handleDrawerToggle}
|
||||
sx={{ mr: 2, display: { sm: 'none' } }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
{navigationItems.find(item => item.path === location.pathname)?.label || 'Dashboard'}
|
||||
</Typography>
|
||||
|
||||
{/* Notifications */}
|
||||
<Tooltip title="Notifications">
|
||||
<IconButton color="inherit">
|
||||
<Badge badgeContent={notifications.length} color="error">
|
||||
<NotificationsIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{/* User Profile */}
|
||||
<Tooltip title="User Profile">
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="end"
|
||||
aria-label="account of current user"
|
||||
aria-controls="primary-search-account-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={handleProfileMenuOpen}
|
||||
color="inherit"
|
||||
>
|
||||
<Avatar sx={{ width: 32, height: 32 }}>
|
||||
{user?.first_name?.[0]}{user?.last_name?.[0]}
|
||||
</Avatar>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleProfileMenuClose}
|
||||
>
|
||||
<MenuItem onClick={handleProfileMenuClose}>
|
||||
<ListItemIcon>
|
||||
<ProfileIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Profile</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleProfileMenuClose}>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Settings</ListItemText>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={handleLogout}>
|
||||
<ListItemIcon>
|
||||
<LogoutIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Logout</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<Box
|
||||
component="nav"
|
||||
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
|
||||
aria-label="mailbox folders"
|
||||
>
|
||||
<Drawer
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
}}
|
||||
sx={{
|
||||
display: { xs: 'block', sm: 'none' },
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: { xs: 'none', sm: 'block' },
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
||||
}}
|
||||
open
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
p: 3,
|
||||
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardLayout;
|
||||
@@ -0,0 +1,850 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Grid,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Avatar,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
BugReport as IncidentIcon,
|
||||
Add as AddIcon,
|
||||
Refresh as RefreshIcon,
|
||||
FilterList as FilterIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Error as ErrorIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
Person as PersonIcon,
|
||||
Security as SecurityIcon,
|
||||
AutoFixHigh as AutoFixIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
Timeline as TimelineIcon,
|
||||
} from '@mui/icons-material';
|
||||
import apiService from '../../services/api';
|
||||
import { Incident } from '../../types';
|
||||
|
||||
interface IncidentIntelligenceDashboardProps {
|
||||
onNavigateToModule: (moduleId: string) => void;
|
||||
}
|
||||
|
||||
interface IncidentStats {
|
||||
total: number;
|
||||
open: number;
|
||||
inProgress: number;
|
||||
resolved: number;
|
||||
critical: number;
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
aiProcessed: number;
|
||||
automationTriggered: number;
|
||||
avgResolutionTime: number;
|
||||
avgResponseTime: number;
|
||||
}
|
||||
|
||||
interface AIMetrics {
|
||||
classificationAccuracy: number;
|
||||
severityPredictionAccuracy: number;
|
||||
duplicateDetectionAccuracy: number;
|
||||
correlationAccuracy: number;
|
||||
processingTime: number;
|
||||
}
|
||||
|
||||
const IncidentIntelligenceDashboard: React.FC<IncidentIntelligenceDashboardProps> = ({
|
||||
onNavigateToModule
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [incidents, setIncidents] = useState<Incident[]>([]);
|
||||
const [stats, setStats] = useState<IncidentStats>({
|
||||
total: 0,
|
||||
open: 0,
|
||||
inProgress: 0,
|
||||
resolved: 0,
|
||||
critical: 0,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0,
|
||||
aiProcessed: 0,
|
||||
automationTriggered: 0,
|
||||
avgResolutionTime: 0,
|
||||
avgResponseTime: 0,
|
||||
});
|
||||
const [aiMetrics, setAiMetrics] = useState<AIMetrics>({
|
||||
classificationAccuracy: 0,
|
||||
severityPredictionAccuracy: 0,
|
||||
duplicateDetectionAccuracy: 0,
|
||||
correlationAccuracy: 0,
|
||||
processingTime: 0,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [, setFilterDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
}, []);
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Load incidents
|
||||
const incidentsResponse = await apiService.getIncidents({ page_size: 50 });
|
||||
setIncidents(incidentsResponse.results);
|
||||
|
||||
// Calculate stats from incidents
|
||||
const total = incidentsResponse.count;
|
||||
const open = incidentsResponse.results.filter(i => i.status === 'OPEN').length;
|
||||
const inProgress = incidentsResponse.results.filter(i => i.status === 'IN_PROGRESS').length;
|
||||
const resolved = incidentsResponse.results.filter(i => i.status === 'RESOLVED' || i.status === 'CLOSED').length;
|
||||
const critical = incidentsResponse.results.filter(i => i.severity === 'CRITICAL' || i.severity === 'EMERGENCY').length;
|
||||
const high = incidentsResponse.results.filter(i => i.severity === 'HIGH').length;
|
||||
const medium = incidentsResponse.results.filter(i => i.severity === 'MEDIUM').length;
|
||||
const low = incidentsResponse.results.filter(i => i.severity === 'LOW').length;
|
||||
const aiProcessed = incidentsResponse.results.filter(i => i.ai_processed).length;
|
||||
const automationTriggered = incidentsResponse.results.filter(i => i.ai_processed).length;
|
||||
|
||||
setStats({
|
||||
total,
|
||||
open,
|
||||
inProgress,
|
||||
resolved,
|
||||
critical,
|
||||
high,
|
||||
medium,
|
||||
low,
|
||||
aiProcessed,
|
||||
automationTriggered,
|
||||
avgResolutionTime: 2.5, // Mock data
|
||||
avgResponseTime: 0.8, // Mock data
|
||||
});
|
||||
|
||||
// Mock AI metrics
|
||||
setAiMetrics({
|
||||
classificationAccuracy: 94.2,
|
||||
severityPredictionAccuracy: 91.8,
|
||||
duplicateDetectionAccuracy: 96.5,
|
||||
correlationAccuracy: 88.7,
|
||||
processingTime: 1.2,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load incident intelligence data:', error);
|
||||
setError('Failed to load incident intelligence 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 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;
|
||||
subtitle?: string;
|
||||
}> = ({ title, value, icon, color, trend, trendValue, subtitle }) => (
|
||||
<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>
|
||||
{subtitle && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{subtitle}
|
||||
</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>
|
||||
);
|
||||
|
||||
const AIMetricCard: React.FC<{
|
||||
title: string;
|
||||
value: number;
|
||||
icon: React.ReactNode;
|
||||
color: string;
|
||||
unit?: string;
|
||||
}> = ({ title, value, icon, color, unit = '%' }) => (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="h6" color="textSecondary">
|
||||
{title}
|
||||
</Typography>
|
||||
<Avatar sx={{ bgcolor: `${color}.main` }}>
|
||||
{icon}
|
||||
</Avatar>
|
||||
</Box>
|
||||
<Typography variant="h4" component="div" color={color}>
|
||||
{value}{unit}
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={value}
|
||||
color={color as any}
|
||||
sx={{ mt: 1, height: 8, borderRadius: 4 }}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box>
|
||||
<LinearProgress />
|
||||
<Typography variant="h6" sx={{ mt: 2 }}>
|
||||
Loading Incident Intelligence Dashboard...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" action={
|
||||
<Button color="inherit" size="small" onClick={loadDashboardData}>
|
||||
Retry
|
||||
</Button>
|
||||
}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header */}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Incident Intelligence
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
AI-powered incident management and analysis
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Create Incident
|
||||
</Button>
|
||||
<IconButton onClick={loadDashboardData}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => setFilterDialogOpen(true)}>
|
||||
<FilterIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Tabs */}
|
||||
<Paper sx={{ mb: 3 }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange} indicatorColor="primary" textColor="primary">
|
||||
<Tab label="Overview" />
|
||||
<Tab label="AI Analytics" />
|
||||
<Tab label="Automation" />
|
||||
<Tab label="Correlations" />
|
||||
</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.total}
|
||||
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.open}
|
||||
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.critical}
|
||||
icon={<ErrorIcon />}
|
||||
color="error"
|
||||
trend="neutral"
|
||||
trendValue="No change"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="AI Processed"
|
||||
value={stats.aiProcessed}
|
||||
icon={<AssessmentIcon />}
|
||||
color="info"
|
||||
trend="up"
|
||||
trendValue="+8% from last week"
|
||||
subtitle={`${Math.round((stats.aiProcessed / stats.total) * 100)}% of total`}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Performance Metrics */}
|
||||
<Grid container spacing={3} mb={3}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Avg Resolution Time"
|
||||
value={`${stats.avgResolutionTime}h`}
|
||||
icon={<ScheduleIcon />}
|
||||
color="secondary"
|
||||
trend="down"
|
||||
trendValue="-0.5h from last week"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Avg Response Time"
|
||||
value={`${stats.avgResponseTime}h`}
|
||||
icon={<PersonIcon />}
|
||||
color="success"
|
||||
trend="down"
|
||||
trendValue="-0.2h from last week"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Automation Triggered"
|
||||
value={stats.automationTriggered}
|
||||
icon={<AutoFixIcon />}
|
||||
color="warning"
|
||||
trend="up"
|
||||
trendValue="+3 from yesterday"
|
||||
subtitle={`${Math.round((stats.automationTriggered / stats.total) * 100)}% of total`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Security Clearance"
|
||||
value="Level 2"
|
||||
icon={<SecurityIcon />}
|
||||
color="info"
|
||||
trend="neutral"
|
||||
trendValue="Required for 12 incidents"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Recent Incidents */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Typography variant="h6">
|
||||
Recent Incidents
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => onNavigateToModule('incident_intelligence')}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</Box>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Title</TableCell>
|
||||
<TableCell>Severity</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>AI Confidence</TableCell>
|
||||
<TableCell>Assigned To</TableCell>
|
||||
<TableCell>Created</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{incidents.slice(0, 10).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.classification_confidence ? (
|
||||
<Box display="flex" alignItems="center">
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={incident.classification_confidence * 100}
|
||||
sx={{ width: 60, mr: 1 }}
|
||||
/>
|
||||
<Typography variant="caption">
|
||||
{Math.round(incident.classification_confidence * 100)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Not processed
|
||||
</Typography>
|
||||
)}
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* AI Analytics Tab */}
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
AI Performance Metrics
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Machine learning model performance and accuracy metrics
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3} mb={3}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<AIMetricCard
|
||||
title="Classification Accuracy"
|
||||
value={aiMetrics.classificationAccuracy}
|
||||
icon={<AssessmentIcon />}
|
||||
color="success"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<AIMetricCard
|
||||
title="Severity Prediction"
|
||||
value={aiMetrics.severityPredictionAccuracy}
|
||||
icon={<WarningIcon />}
|
||||
color="warning"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<AIMetricCard
|
||||
title="Duplicate Detection"
|
||||
value={aiMetrics.duplicateDetectionAccuracy}
|
||||
icon={<CheckCircleIcon />}
|
||||
color="info"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<AIMetricCard
|
||||
title="Correlation Accuracy"
|
||||
value={aiMetrics.correlationAccuracy}
|
||||
icon={<TimelineIcon />}
|
||||
color="secondary"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
AI Processing Stats
|
||||
</Typography>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<AssessmentIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Average Processing Time"
|
||||
secondary={`${aiMetrics.processingTime}s per incident`}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Successful Classifications"
|
||||
secondary={`${stats.aiProcessed} of ${stats.total} incidents`}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<WarningIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Manual Overrides"
|
||||
secondary="12 classifications reviewed"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Model Performance Trends
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" paragraph>
|
||||
AI models are performing above target thresholds across all metrics.
|
||||
Classification accuracy has improved by 2.3% this month.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => onNavigateToModule('analytics_predictive_insights')}
|
||||
>
|
||||
View Detailed Analytics
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Automation Tab */}
|
||||
{activeTab === 2 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Automation & Orchestration
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Automated incident response and runbook execution
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Automation Statistics
|
||||
</Typography>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<AutoFixIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Automation Triggered"
|
||||
secondary={`${stats.automationTriggered} times`}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Successful Executions"
|
||||
secondary="89% success rate"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<ScheduleIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Average Execution Time"
|
||||
secondary="2.3 minutes"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Runbook Suggestions
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" paragraph>
|
||||
AI has suggested runbooks for {stats.automationTriggered} incidents this week.
|
||||
Automation has reduced manual intervention by 34%.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => onNavigateToModule('automation_orchestration')}
|
||||
>
|
||||
Manage Automation
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Correlations Tab */}
|
||||
{activeTab === 3 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Incident Correlations
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
AI-detected patterns and relationships between incidents
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Detected Patterns
|
||||
</Typography>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<TimelineIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Service Dependencies"
|
||||
secondary="3 patterns identified"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<WarningIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Recurring Issues"
|
||||
secondary="7 recurring patterns"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<AssessmentIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Cascade Effects"
|
||||
secondary="2 cascade patterns"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Correlation Accuracy
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" paragraph>
|
||||
AI correlation engine has achieved {aiMetrics.correlationAccuracy}% accuracy
|
||||
in identifying related incidents. This helps reduce duplicate work and
|
||||
identify root causes faster.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => onNavigateToModule('analytics_predictive_insights')}
|
||||
>
|
||||
View Correlation Details
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Create Incident Dialog */}
|
||||
<Dialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Create New Incident</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label="Incident Title"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label="Description"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
variant="outlined"
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Severity</InputLabel>
|
||||
<Select label="Severity">
|
||||
<MenuItem value="LOW">Low</MenuItem>
|
||||
<MenuItem value="MEDIUM">Medium</MenuItem>
|
||||
<MenuItem value="HIGH">High</MenuItem>
|
||||
<MenuItem value="CRITICAL">Critical</MenuItem>
|
||||
<MenuItem value="EMERGENCY">Emergency</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Priority</InputLabel>
|
||||
<Select label="Priority">
|
||||
<MenuItem value="P4">P4 - Low</MenuItem>
|
||||
<MenuItem value="P3">P3 - Medium</MenuItem>
|
||||
<MenuItem value="P2">P2 - High</MenuItem>
|
||||
<MenuItem value="P1">P1 - Critical</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCreateDialogOpen(false)}>Cancel</Button>
|
||||
<Button variant="contained" onClick={() => setCreateDialogOpen(false)}>
|
||||
Create Incident
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default IncidentIntelligenceDashboard;
|
||||
972
etb-dashboard/src/components/Dashboard/KnowledgeDashboard.tsx
Normal file
972
etb-dashboard/src/components/Dashboard/KnowledgeDashboard.tsx
Normal file
@@ -0,0 +1,972 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Grid,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Button,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Avatar,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Tabs,
|
||||
Tab,
|
||||
Rating,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Article as ArticleIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Settings as SettingsIcon,
|
||||
Upload as UploadIcon,
|
||||
Warning as WarningIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Visibility as VisibilityIcon,
|
||||
ThumbUp as ThumbUpIcon,
|
||||
Edit as EditIcon,
|
||||
Share as ShareIcon,
|
||||
Bookmark as BookmarkIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface KnowledgeDashboardProps {
|
||||
onNavigateToModule: (moduleId: string) => void;
|
||||
}
|
||||
|
||||
interface KnowledgeStats {
|
||||
totalArticles: number;
|
||||
publishedArticles: number;
|
||||
draftArticles: number;
|
||||
totalViews: number;
|
||||
totalLikes: number;
|
||||
avgRating: number;
|
||||
searchQueries: number;
|
||||
knowledgeGaps: number;
|
||||
}
|
||||
|
||||
interface KnowledgeArticle {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
author: string;
|
||||
status: 'PUBLISHED' | 'DRAFT' | 'REVIEW' | 'ARCHIVED';
|
||||
views: number;
|
||||
likes: number;
|
||||
rating: number;
|
||||
lastUpdated: string;
|
||||
wordCount: number;
|
||||
readingTime: number;
|
||||
isBookmarked: boolean;
|
||||
}
|
||||
|
||||
interface LearningModule {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'INCIDENT_RESPONSE' | 'AUTOMATION' | 'SECURITY' | 'MONITORING' | 'GENERAL';
|
||||
difficulty: 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED' | 'EXPERT';
|
||||
duration: number;
|
||||
completionRate: number;
|
||||
rating: number;
|
||||
enrolledUsers: number;
|
||||
lastUpdated: string;
|
||||
prerequisites: string[];
|
||||
skills: string[];
|
||||
}
|
||||
|
||||
interface KnowledgeGap {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
requestedBy: string;
|
||||
requestedAt: string;
|
||||
status: 'OPEN' | 'IN_PROGRESS' | 'RESOLVED' | 'CLOSED';
|
||||
relatedIncidents: string[];
|
||||
estimatedEffort: string;
|
||||
}
|
||||
|
||||
interface SearchQuery {
|
||||
id: string;
|
||||
query: string;
|
||||
timestamp: string;
|
||||
resultsFound: number;
|
||||
user: string;
|
||||
category: string;
|
||||
successRate: number;
|
||||
}
|
||||
|
||||
const KnowledgeDashboard: React.FC<KnowledgeDashboardProps> = ({ onNavigateToModule }) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [stats, setStats] = useState<KnowledgeStats>({
|
||||
totalArticles: 0,
|
||||
publishedArticles: 0,
|
||||
draftArticles: 0,
|
||||
totalViews: 0,
|
||||
totalLikes: 0,
|
||||
avgRating: 0,
|
||||
searchQueries: 0,
|
||||
knowledgeGaps: 0,
|
||||
});
|
||||
const [articles, setArticles] = useState<KnowledgeArticle[]>([]);
|
||||
const [learningModules, setLearningModules] = useState<LearningModule[]>([]);
|
||||
const [knowledgeGaps, setKnowledgeGaps] = useState<KnowledgeGap[]>([]);
|
||||
const [searchQueries, setSearchQueries] = useState<SearchQuery[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadKnowledgeData();
|
||||
}, []);
|
||||
|
||||
const loadKnowledgeData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Mock data - replace with actual API calls
|
||||
setStats({
|
||||
totalArticles: 234,
|
||||
publishedArticles: 189,
|
||||
draftArticles: 45,
|
||||
totalViews: 15420,
|
||||
totalLikes: 892,
|
||||
avgRating: 4.2,
|
||||
searchQueries: 567,
|
||||
knowledgeGaps: 12,
|
||||
});
|
||||
|
||||
setArticles([
|
||||
{
|
||||
id: '1',
|
||||
title: 'Database Performance Optimization Guide',
|
||||
description: 'Comprehensive guide to optimizing database performance and troubleshooting common issues',
|
||||
category: 'Database',
|
||||
tags: ['performance', 'optimization', 'troubleshooting'],
|
||||
author: 'John Doe',
|
||||
status: 'PUBLISHED',
|
||||
views: 245,
|
||||
likes: 18,
|
||||
rating: 4.5,
|
||||
lastUpdated: '2024-01-15T10:30:00Z',
|
||||
wordCount: 2500,
|
||||
readingTime: 12,
|
||||
isBookmarked: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Incident Response Best Practices',
|
||||
description: 'Step-by-step guide for effective incident response and management',
|
||||
category: 'Incident Management',
|
||||
tags: ['incident-response', 'best-practices', 'process'],
|
||||
author: 'Jane Smith',
|
||||
status: 'PUBLISHED',
|
||||
views: 189,
|
||||
likes: 15,
|
||||
rating: 4.8,
|
||||
lastUpdated: '2024-01-14T15:20:00Z',
|
||||
wordCount: 3200,
|
||||
readingTime: 16,
|
||||
isBookmarked: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'API Gateway Configuration',
|
||||
description: 'How to configure and optimize API gateway settings for better performance',
|
||||
category: 'Infrastructure',
|
||||
tags: ['api-gateway', 'configuration', 'performance'],
|
||||
author: 'Mike Johnson',
|
||||
status: 'DRAFT',
|
||||
views: 0,
|
||||
likes: 0,
|
||||
rating: 0,
|
||||
lastUpdated: '2024-01-15T09:15:00Z',
|
||||
wordCount: 1800,
|
||||
readingTime: 9,
|
||||
isBookmarked: false,
|
||||
},
|
||||
]);
|
||||
|
||||
setLearningModules([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Advanced Incident Response',
|
||||
description: 'Master the art of incident response with advanced techniques and tools',
|
||||
category: 'INCIDENT_RESPONSE',
|
||||
difficulty: 'ADVANCED',
|
||||
duration: 180,
|
||||
completionRate: 78,
|
||||
rating: 4.6,
|
||||
enrolledUsers: 45,
|
||||
lastUpdated: '2024-01-15T08:00:00Z',
|
||||
prerequisites: ['Basic Incident Response', 'Communication Skills'],
|
||||
skills: ['Incident Command', 'Team Coordination', 'Root Cause Analysis'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Automation Fundamentals',
|
||||
description: 'Learn the basics of automation and orchestration in incident management',
|
||||
category: 'AUTOMATION',
|
||||
difficulty: 'BEGINNER',
|
||||
duration: 120,
|
||||
completionRate: 92,
|
||||
rating: 4.3,
|
||||
enrolledUsers: 67,
|
||||
lastUpdated: '2024-01-14T16:30:00Z',
|
||||
prerequisites: [],
|
||||
skills: ['Runbook Creation', 'Workflow Design', 'Tool Integration'],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Security Incident Handling',
|
||||
description: 'Specialized training for handling security-related incidents',
|
||||
category: 'SECURITY',
|
||||
difficulty: 'EXPERT',
|
||||
duration: 240,
|
||||
completionRate: 65,
|
||||
rating: 4.7,
|
||||
enrolledUsers: 23,
|
||||
lastUpdated: '2024-01-13T14:15:00Z',
|
||||
prerequisites: ['Security Fundamentals', 'Incident Response'],
|
||||
skills: ['Threat Analysis', 'Forensic Investigation', 'Compliance'],
|
||||
},
|
||||
]);
|
||||
|
||||
setKnowledgeGaps([
|
||||
{
|
||||
id: '1',
|
||||
title: 'Kubernetes Troubleshooting Guide',
|
||||
description: 'Need comprehensive guide for troubleshooting Kubernetes cluster issues',
|
||||
category: 'Infrastructure',
|
||||
priority: 'HIGH',
|
||||
requestedBy: 'Sarah Wilson',
|
||||
requestedAt: '2024-01-15T11:00:00Z',
|
||||
status: 'OPEN',
|
||||
relatedIncidents: ['INC-001', 'INC-005'],
|
||||
estimatedEffort: '2 weeks',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Microservices Communication Patterns',
|
||||
description: 'Documentation needed for microservices communication best practices',
|
||||
category: 'Architecture',
|
||||
priority: 'MEDIUM',
|
||||
requestedBy: 'David Brown',
|
||||
requestedAt: '2024-01-14T16:45:00Z',
|
||||
status: 'IN_PROGRESS',
|
||||
relatedIncidents: ['INC-003'],
|
||||
estimatedEffort: '1 week',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Cloud Cost Optimization',
|
||||
description: 'Guide for optimizing cloud costs and resource utilization',
|
||||
category: 'Finance',
|
||||
priority: 'LOW',
|
||||
requestedBy: 'Lisa Chen',
|
||||
requestedAt: '2024-01-13T09:30:00Z',
|
||||
status: 'OPEN',
|
||||
relatedIncidents: [],
|
||||
estimatedEffort: '3 weeks',
|
||||
},
|
||||
]);
|
||||
|
||||
setSearchQueries([
|
||||
{
|
||||
id: '1',
|
||||
query: 'database connection timeout',
|
||||
timestamp: '2024-01-15T11:25:00Z',
|
||||
resultsFound: 12,
|
||||
user: 'John Doe',
|
||||
category: 'Database',
|
||||
successRate: 85,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
query: 'API gateway configuration',
|
||||
timestamp: '2024-01-15T10:45:00Z',
|
||||
resultsFound: 8,
|
||||
user: 'Mike Johnson',
|
||||
category: 'Infrastructure',
|
||||
successRate: 92,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
query: 'incident response checklist',
|
||||
timestamp: '2024-01-15T09:30:00Z',
|
||||
resultsFound: 15,
|
||||
user: 'Jane Smith',
|
||||
category: 'Incident Management',
|
||||
successRate: 78,
|
||||
},
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load knowledge data:', error);
|
||||
setError('Failed to load knowledge data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'PUBLISHED':
|
||||
case 'RESOLVED':
|
||||
return 'success';
|
||||
case 'DRAFT':
|
||||
case 'IN_PROGRESS':
|
||||
return 'warning';
|
||||
case 'REVIEW':
|
||||
case 'OPEN':
|
||||
return 'info';
|
||||
case 'ARCHIVED':
|
||||
case 'CLOSED':
|
||||
return 'default';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'CRITICAL':
|
||||
return 'error';
|
||||
case 'HIGH':
|
||||
return 'warning';
|
||||
case 'MEDIUM':
|
||||
return 'info';
|
||||
case 'LOW':
|
||||
return 'success';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case 'EXPERT':
|
||||
return 'error';
|
||||
case 'ADVANCED':
|
||||
return 'warning';
|
||||
case 'INTERMEDIATE':
|
||||
return 'info';
|
||||
case 'BEGINNER':
|
||||
return 'success';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
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;
|
||||
subtitle?: string;
|
||||
}> = ({ title, value, icon, color, trend, trendValue, subtitle }) => (
|
||||
<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>
|
||||
{subtitle && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
{trend && trendValue && (
|
||||
<Box display="flex" alignItems="center" mt={1}>
|
||||
{trend === 'up' ? (
|
||||
<TrendingUpIcon color="success" fontSize="small" />
|
||||
) : trend === 'down' ? (
|
||||
<TrendingDownIcon color="error" 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 Knowledge Dashboard...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" action={
|
||||
<Button color="inherit" size="small" onClick={loadKnowledgeData}>
|
||||
Retry
|
||||
</Button>
|
||||
}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header */}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Knowledge & Learning
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
Centralized knowledge base and learning management system
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<UploadIcon />}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Add Article
|
||||
</Button>
|
||||
<IconButton onClick={loadKnowledgeData}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Knowledge Status Alert */}
|
||||
<Alert
|
||||
severity={stats.knowledgeGaps > 10 ? 'warning' : 'success'}
|
||||
sx={{ mb: 3 }}
|
||||
icon={stats.knowledgeGaps > 10 ? <WarningIcon /> : <CheckCircleIcon />}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
Knowledge Base Status: {stats.publishedArticles} Articles Published
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{stats.knowledgeGaps > 10 ? 'High number of knowledge gaps identified. Consider prioritizing content creation.' : 'Knowledge base is well-maintained with comprehensive coverage.'}
|
||||
{stats.totalViews > 0 && ` ${stats.totalViews} total views this month.`}
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
{/* Tabs */}
|
||||
<Paper sx={{ mb: 3 }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange} indicatorColor="primary" textColor="primary">
|
||||
<Tab label="Overview" />
|
||||
<Tab label="Knowledge Base" />
|
||||
<Tab label="Learning Modules" />
|
||||
<Tab label="Knowledge Gaps" />
|
||||
<Tab label="Search Analytics" />
|
||||
</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 Articles"
|
||||
value={stats.totalArticles}
|
||||
icon={<ArticleIcon />}
|
||||
color="primary"
|
||||
trend="up"
|
||||
trendValue="+12 this week"
|
||||
subtitle={`${stats.publishedArticles} published`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Total Views"
|
||||
value={stats.totalViews}
|
||||
icon={<VisibilityIcon />}
|
||||
color="info"
|
||||
trend="up"
|
||||
trendValue="+234 this week"
|
||||
subtitle="Knowledge engagement"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Avg Rating"
|
||||
value={stats.avgRating}
|
||||
icon={<ThumbUpIcon />}
|
||||
color="success"
|
||||
trend="up"
|
||||
trendValue="+0.2 this month"
|
||||
subtitle="Content quality"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Knowledge Gaps"
|
||||
value={stats.knowledgeGaps}
|
||||
icon={<WarningIcon />}
|
||||
color="warning"
|
||||
trend="down"
|
||||
trendValue="-2 this week"
|
||||
subtitle="Identified gaps"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Recent Articles */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Recent Articles
|
||||
</Typography>
|
||||
<List>
|
||||
{articles.slice(0, 3).map((article) => (
|
||||
<ListItem key={article.id}>
|
||||
<ListItemIcon>
|
||||
<ArticleIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Typography variant="body1" fontWeight="bold">
|
||||
{article.title}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={article.status}
|
||||
color={getStatusColor(article.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{article.description}
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" mt={1}>
|
||||
<Typography variant="caption" sx={{ mr: 2 }}>
|
||||
By {article.author}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ mr: 2 }}>
|
||||
{article.views} views
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ mr: 2 }}>
|
||||
{article.likes} likes
|
||||
</Typography>
|
||||
<Rating value={article.rating} size="small" readOnly />
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Knowledge Base Tab */}
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Knowledge Base
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Manage and organize knowledge articles and documentation
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Title</TableCell>
|
||||
<TableCell>Category</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Author</TableCell>
|
||||
<TableCell>Views</TableCell>
|
||||
<TableCell>Rating</TableCell>
|
||||
<TableCell>Last Updated</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{articles.map((article) => (
|
||||
<TableRow key={article.id}>
|
||||
<TableCell>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{article.title}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{article.description}
|
||||
</Typography>
|
||||
<Box sx={{ mt: 0.5 }}>
|
||||
{article.tags.map((tag, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={tag}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ mr: 0.5, mb: 0.5 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={article.category} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={article.status}
|
||||
color={getStatusColor(article.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{article.author}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{article.views}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Rating value={article.rating} size="small" readOnly />
|
||||
<Typography variant="caption" sx={{ ml: 1 }}>
|
||||
({article.likes})
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(article.lastUpdated)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box>
|
||||
<IconButton size="small">
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small">
|
||||
<ShareIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small">
|
||||
{article.isBookmarked ? <BookmarkIcon color="primary" /> : <BookmarkIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Learning Modules Tab */}
|
||||
{activeTab === 2 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Learning Modules
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Training modules and educational content for skill development
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3} sx={{ mt: 2 }}>
|
||||
{learningModules.map((module) => (
|
||||
<Grid size={{ xs: 12, md: 6, lg: 4 }} key={module.id}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="start" mb={2}>
|
||||
<Typography variant="h6">
|
||||
{module.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={module.difficulty}
|
||||
color={getDifficultyColor(module.difficulty) as any}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body2" color="textSecondary" paragraph>
|
||||
{module.description}
|
||||
</Typography>
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
Progress:
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={module.completionRate}
|
||||
color={module.completionRate > 80 ? 'success' : module.completionRate > 60 ? 'warning' : 'error'}
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
<Typography variant="caption">
|
||||
{module.completionRate}% completion rate
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="caption">
|
||||
Duration: {module.duration} min
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
Enrolled: {module.enrolledUsers}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<Box display="flex" alignItems="center" mb={1}>
|
||||
<Rating value={module.rating} size="small" readOnly />
|
||||
<Typography variant="caption" sx={{ ml: 1 }}>
|
||||
{module.rating}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
Skills:
|
||||
</Typography>
|
||||
<Box sx={{ mt: 0.5 }}>
|
||||
{module.skills.map((skill, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={skill}
|
||||
size="small"
|
||||
sx={{ mr: 0.5, mb: 0.5 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
fullWidth
|
||||
>
|
||||
Enroll
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Knowledge Gaps Tab */}
|
||||
{activeTab === 3 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Knowledge Gaps
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Identify and prioritize missing knowledge areas
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Title</TableCell>
|
||||
<TableCell>Category</TableCell>
|
||||
<TableCell>Priority</TableCell>
|
||||
<TableCell>Requested By</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Related Incidents</TableCell>
|
||||
<TableCell>Effort</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{knowledgeGaps.map((gap) => (
|
||||
<TableRow key={gap.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{gap.title}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{gap.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={gap.category} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={gap.priority}
|
||||
color={getPriorityColor(gap.priority) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{gap.requestedBy}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={gap.status}
|
||||
color={getStatusColor(gap.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{gap.relatedIncidents.length > 0 ? gap.relatedIncidents.join(', ') : 'None'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{gap.estimatedEffort}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Create Article
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Search Analytics Tab */}
|
||||
{activeTab === 4 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Search Analytics
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Analyze search patterns and knowledge base effectiveness
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Search Query</TableCell>
|
||||
<TableCell>Category</TableCell>
|
||||
<TableCell>Results Found</TableCell>
|
||||
<TableCell>Success Rate</TableCell>
|
||||
<TableCell>User</TableCell>
|
||||
<TableCell>Timestamp</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{searchQueries.map((query) => (
|
||||
<TableRow key={query.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{query.query}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={query.category} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{query.resultsFound}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={query.successRate}
|
||||
color={query.successRate > 80 ? 'success' : query.successRate > 60 ? 'warning' : 'error'}
|
||||
sx={{ width: 60, mr: 1 }}
|
||||
/>
|
||||
<Typography variant="caption">
|
||||
{query.successRate}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{query.user}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(query.timestamp)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Analyze
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeDashboard;
|
||||
497
etb-dashboard/src/components/Dashboard/ModuleOverviewCards.tsx
Normal file
497
etb-dashboard/src/components/Dashboard/ModuleOverviewCards.tsx
Normal file
@@ -0,0 +1,497 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Box,
|
||||
Chip,
|
||||
Avatar,
|
||||
LinearProgress,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Grid,
|
||||
Paper,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
BugReport as IncidentIcon,
|
||||
Schedule as SLIcon,
|
||||
Security as SecurityIcon,
|
||||
Monitor as MonitorIcon,
|
||||
Group as CollaborationIcon,
|
||||
AutoFixHigh as AutomationIcon,
|
||||
Assessment as AnalyticsIcon,
|
||||
School as KnowledgeIcon,
|
||||
Gavel as ComplianceIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Error as ErrorIcon,
|
||||
ArrowForward as ArrowForwardIcon,
|
||||
Link as LinkIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface ModuleStats {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'healthy' | 'warning' | 'critical' | 'maintenance';
|
||||
healthScore: number;
|
||||
activeItems: number;
|
||||
totalItems: number;
|
||||
responseTime: number;
|
||||
lastUpdated: string;
|
||||
trends: {
|
||||
incidents: 'up' | 'down' | 'stable';
|
||||
performance: 'up' | 'down' | 'stable';
|
||||
usage: 'up' | 'down' | 'stable';
|
||||
};
|
||||
}
|
||||
|
||||
interface ModuleRelationship {
|
||||
source: string;
|
||||
target: string;
|
||||
type: 'data_flow' | 'dependency' | 'integration' | 'escalation';
|
||||
strength: 'weak' | 'medium' | 'strong';
|
||||
}
|
||||
|
||||
interface ModuleOverviewCardsProps {
|
||||
onModuleClick: (moduleId: string) => void;
|
||||
}
|
||||
|
||||
const ModuleOverviewCards: React.FC<ModuleOverviewCardsProps> = ({ onModuleClick }) => {
|
||||
// Mock data - this would come from your backend API
|
||||
const moduleStats: ModuleStats[] = [
|
||||
{
|
||||
id: 'incident_intelligence',
|
||||
name: 'Incident Intelligence',
|
||||
status: 'healthy',
|
||||
healthScore: 95,
|
||||
activeItems: 12,
|
||||
totalItems: 156,
|
||||
responseTime: 120,
|
||||
lastUpdated: '2024-01-15T10:30:00Z',
|
||||
trends: { incidents: 'down', performance: 'up', usage: 'stable' }
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
name: 'Security & Access',
|
||||
status: 'healthy',
|
||||
healthScore: 98,
|
||||
activeItems: 3,
|
||||
totalItems: 45,
|
||||
responseTime: 85,
|
||||
lastUpdated: '2024-01-15T10:25:00Z',
|
||||
trends: { incidents: 'stable', performance: 'up', usage: 'up' }
|
||||
},
|
||||
{
|
||||
id: 'monitoring',
|
||||
name: 'System Monitoring',
|
||||
status: 'warning',
|
||||
healthScore: 87,
|
||||
activeItems: 8,
|
||||
totalItems: 234,
|
||||
responseTime: 200,
|
||||
lastUpdated: '2024-01-15T10:20:00Z',
|
||||
trends: { incidents: 'up', performance: 'down', usage: 'up' }
|
||||
},
|
||||
{
|
||||
id: 'sla_oncall',
|
||||
name: 'SLA & On-Call',
|
||||
status: 'healthy',
|
||||
healthScore: 92,
|
||||
activeItems: 5,
|
||||
totalItems: 78,
|
||||
responseTime: 95,
|
||||
lastUpdated: '2024-01-15T10:28:00Z',
|
||||
trends: { incidents: 'stable', performance: 'stable', usage: 'stable' }
|
||||
},
|
||||
{
|
||||
id: 'automation_orchestration',
|
||||
name: 'Automation',
|
||||
status: 'healthy',
|
||||
healthScore: 89,
|
||||
activeItems: 15,
|
||||
totalItems: 67,
|
||||
responseTime: 150,
|
||||
lastUpdated: '2024-01-15T10:22:00Z',
|
||||
trends: { incidents: 'down', performance: 'up', usage: 'up' }
|
||||
},
|
||||
{
|
||||
id: 'collaboration_war_rooms',
|
||||
name: 'War Rooms',
|
||||
status: 'healthy',
|
||||
healthScore: 94,
|
||||
activeItems: 2,
|
||||
totalItems: 23,
|
||||
responseTime: 110,
|
||||
lastUpdated: '2024-01-15T10:26:00Z',
|
||||
trends: { incidents: 'stable', performance: 'stable', usage: 'down' }
|
||||
},
|
||||
{
|
||||
id: 'analytics_predictive_insights',
|
||||
name: 'Analytics & AI',
|
||||
status: 'healthy',
|
||||
healthScore: 96,
|
||||
activeItems: 7,
|
||||
totalItems: 89,
|
||||
responseTime: 180,
|
||||
lastUpdated: '2024-01-15T10:24:00Z',
|
||||
trends: { incidents: 'down', performance: 'up', usage: 'up' }
|
||||
},
|
||||
{
|
||||
id: 'knowledge_learning',
|
||||
name: 'Knowledge Base',
|
||||
status: 'healthy',
|
||||
healthScore: 91,
|
||||
activeItems: 4,
|
||||
totalItems: 156,
|
||||
responseTime: 75,
|
||||
lastUpdated: '2024-01-15T10:27:00Z',
|
||||
trends: { incidents: 'stable', performance: 'stable', usage: 'up' }
|
||||
},
|
||||
{
|
||||
id: 'compliance_governance',
|
||||
name: 'Compliance',
|
||||
status: 'healthy',
|
||||
healthScore: 97,
|
||||
activeItems: 1,
|
||||
totalItems: 34,
|
||||
responseTime: 90,
|
||||
lastUpdated: '2024-01-15T10:29:00Z',
|
||||
trends: { incidents: 'stable', performance: 'stable', usage: 'stable' }
|
||||
}
|
||||
];
|
||||
|
||||
const moduleRelationships: ModuleRelationship[] = [
|
||||
{ source: 'incident_intelligence', target: 'security', type: 'data_flow', strength: 'strong' },
|
||||
{ source: 'incident_intelligence', target: 'monitoring', type: 'dependency', strength: 'strong' },
|
||||
{ source: 'incident_intelligence', target: 'sla_oncall', type: 'escalation', strength: 'strong' },
|
||||
{ source: 'incident_intelligence', target: 'automation_orchestration', type: 'integration', strength: 'medium' },
|
||||
{ source: 'incident_intelligence', target: 'collaboration_war_rooms', type: 'data_flow', strength: 'strong' },
|
||||
{ source: 'security', target: 'compliance_governance', type: 'data_flow', strength: 'strong' },
|
||||
{ source: 'monitoring', target: 'analytics_predictive_insights', type: 'data_flow', strength: 'strong' },
|
||||
{ source: 'automation_orchestration', target: 'knowledge_learning', type: 'data_flow', strength: 'medium' },
|
||||
{ source: 'collaboration_war_rooms', target: 'knowledge_learning', type: 'data_flow', strength: 'medium' },
|
||||
];
|
||||
|
||||
const getModuleIcon = (moduleId: string) => {
|
||||
const iconMap: { [key: string]: React.ComponentType } = {
|
||||
incident_intelligence: IncidentIcon,
|
||||
security: SecurityIcon,
|
||||
monitoring: MonitorIcon,
|
||||
sla_oncall: SLIcon,
|
||||
automation_orchestration: AutomationIcon,
|
||||
collaboration_war_rooms: CollaborationIcon,
|
||||
analytics_predictive_insights: AnalyticsIcon,
|
||||
knowledge_learning: KnowledgeIcon,
|
||||
compliance_governance: ComplianceIcon,
|
||||
};
|
||||
return iconMap[moduleId] || IncidentIcon;
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy': return 'success';
|
||||
case 'warning': return 'warning';
|
||||
case 'critical': return 'error';
|
||||
case 'maintenance': return 'info';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy': return <CheckCircleIcon color="success" />;
|
||||
case 'warning': return <WarningIcon color="warning" />;
|
||||
case 'critical': return <ErrorIcon color="error" />;
|
||||
case 'maintenance': return <WarningIcon color="info" />;
|
||||
default: return <CheckCircleIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
const getTrendIcon = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'up': return <TrendingUpIcon color="success" fontSize="small" />;
|
||||
case 'down': return <TrendingDownIcon color="error" fontSize="small" />;
|
||||
case 'stable': return <div style={{ width: 16, height: 16 }} />;
|
||||
default: return <div style={{ width: 16, height: 16 }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getRelationshipStrength = (strength: string) => {
|
||||
switch (strength) {
|
||||
case 'strong': return { width: 3, opacity: 1 };
|
||||
case 'medium': return { width: 2, opacity: 0.7 };
|
||||
case 'weak': return { width: 1, opacity: 0.4 };
|
||||
default: return { width: 1, opacity: 0.4 };
|
||||
}
|
||||
};
|
||||
|
||||
const getRelationshipColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'data_flow': return '#1976d2';
|
||||
case 'dependency': return '#ed6c02';
|
||||
case 'integration': return '#2e7d32';
|
||||
case 'escalation': return '#d32f2f';
|
||||
default: return '#666';
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
|
||||
if (diffMins < 1) return 'Just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
return `${diffDays}d ago`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Module Overview Header */}
|
||||
<Box mb={3}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Enterprise Module Overview
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Real-time status and relationships between all system modules
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Module Cards Grid */}
|
||||
<Grid container spacing={3} mb={4}>
|
||||
{moduleStats.map((module) => {
|
||||
const IconComponent = getModuleIcon(module.id);
|
||||
const moduleRelations = moduleRelationships.filter(
|
||||
rel => rel.source === module.id || rel.target === module.id
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={module.id}>
|
||||
<Card
|
||||
sx={{
|
||||
height: '100%',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: 4,
|
||||
}
|
||||
}}
|
||||
onClick={() => onModuleClick(module.id)}
|
||||
>
|
||||
<CardContent>
|
||||
{/* Module Header */}
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Avatar sx={{ bgcolor: `${getStatusColor(module.status)}.main`, mr: 2 }}>
|
||||
<IconComponent />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h6" component="div">
|
||||
{module.name}
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center">
|
||||
{getStatusIcon(module.status)}
|
||||
<Chip
|
||||
label={module.status.toUpperCase()}
|
||||
color={getStatusColor(module.status) as any}
|
||||
size="small"
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<IconButton size="small" onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onModuleClick(module.id);
|
||||
}}>
|
||||
<ArrowForwardIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Health Score */}
|
||||
<Box mb={2}>
|
||||
<Box display="flex" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="body2">Health Score</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{module.healthScore}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={module.healthScore}
|
||||
color={getStatusColor(module.status) as any}
|
||||
sx={{ height: 8, borderRadius: 4 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Key Metrics */}
|
||||
<Box mb={2}>
|
||||
<Grid container spacing={1}>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<Paper variant="outlined" sx={{ p: 1, textAlign: 'center' }}>
|
||||
<Typography variant="h6" color="primary">
|
||||
{module.activeItems}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Active
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<Paper variant="outlined" sx={{ p: 1, textAlign: 'center' }}>
|
||||
<Typography variant="h6" color="secondary">
|
||||
{module.totalItems}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Total
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Performance Metrics */}
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Response Time: {module.responseTime}ms
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Last Updated: {formatTime(module.lastUpdated)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Trends */}
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Trends
|
||||
</Typography>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Box display="flex" alignItems="center">
|
||||
{getTrendIcon(module.trends.incidents)}
|
||||
<Typography variant="caption" sx={{ ml: 0.5 }}>
|
||||
Incidents
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center">
|
||||
{getTrendIcon(module.trends.performance)}
|
||||
<Typography variant="caption" sx={{ ml: 0.5 }}>
|
||||
Performance
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center">
|
||||
{getTrendIcon(module.trends.usage)}
|
||||
<Typography variant="caption" sx={{ ml: 0.5 }}>
|
||||
Usage
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Relationships Indicator */}
|
||||
{moduleRelations.length > 0 && (
|
||||
<Box mt={2}>
|
||||
<Divider sx={{ mb: 1 }} />
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{moduleRelations.length} Connected Modules
|
||||
</Typography>
|
||||
<Tooltip title="View Module Relationships">
|
||||
<IconButton size="small">
|
||||
<LinkIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
{/* Module Relationships Visualization */}
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Module Relationships & Data Flow
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Visual representation of how modules interact and share data
|
||||
</Typography>
|
||||
|
||||
{/* Simplified relationship diagram */}
|
||||
<Box sx={{ mt: 2, p: 2, bgcolor: 'grey.50', borderRadius: 1 }}>
|
||||
<Grid container spacing={2}>
|
||||
{moduleRelationships.map((rel, index) => {
|
||||
const sourceModule = moduleStats.find(m => m.id === rel.source);
|
||||
const targetModule = moduleStats.find(m => m.id === rel.target);
|
||||
const strength = getRelationshipStrength(rel.strength);
|
||||
const color = getRelationshipColor(rel.type);
|
||||
|
||||
return (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
p: 1,
|
||||
borderLeft: `${strength.width}px solid ${color}`,
|
||||
borderLeftOpacity: strength.opacity,
|
||||
bgcolor: 'white',
|
||||
borderRadius: 1,
|
||||
mb: 1
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ flex: 1 }}>
|
||||
<strong>{sourceModule?.name}</strong> → <strong>{targetModule?.name}</strong>
|
||||
</Typography>
|
||||
<Chip
|
||||
label={rel.type.replace('_', ' ')}
|
||||
size="small"
|
||||
sx={{
|
||||
ml: 1,
|
||||
bgcolor: `${color}20`,
|
||||
color: color,
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Legend */}
|
||||
<Box sx={{ mt: 2, display: 'flex', flexWrap: 'wrap', gap: 2 }}>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box sx={{ width: 20, height: 3, bgcolor: '#1976d2', mr: 1 }} />
|
||||
<Typography variant="caption">Data Flow</Typography>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box sx={{ width: 20, height: 3, bgcolor: '#ed6c02', mr: 1 }} />
|
||||
<Typography variant="caption">Dependency</Typography>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box sx={{ width: 20, height: 3, bgcolor: '#2e7d32', mr: 1 }} />
|
||||
<Typography variant="caption">Integration</Typography>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box sx={{ width: 20, height: 3, bgcolor: '#d32f2f', mr: 1 }} />
|
||||
<Typography variant="caption">Escalation</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModuleOverviewCards;
|
||||
855
etb-dashboard/src/components/Dashboard/MonitoringDashboard.tsx
Normal file
855
etb-dashboard/src/components/Dashboard/MonitoringDashboard.tsx
Normal file
@@ -0,0 +1,855 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Grid,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Button,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Avatar,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Monitor as MonitorIcon,
|
||||
Speed as SpeedIcon,
|
||||
Memory as MemoryIcon,
|
||||
Cloud as CloudIcon,
|
||||
Warning as WarningIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Settings as SettingsIcon,
|
||||
Computer as ComputerIcon,
|
||||
Storage as DatabaseIcon,
|
||||
Queue as QueueIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface MonitoringDashboardProps {
|
||||
onNavigateToModule: (moduleId: string) => void;
|
||||
}
|
||||
|
||||
interface MonitoringStats {
|
||||
totalTargets: number;
|
||||
healthyTargets: number;
|
||||
warningTargets: number;
|
||||
criticalTargets: number;
|
||||
systemHealth: number;
|
||||
avgResponseTime: number;
|
||||
activeAlerts: number;
|
||||
resolvedAlerts: number;
|
||||
dataRetention: number;
|
||||
lastHealthCheck: string;
|
||||
}
|
||||
|
||||
interface HealthCheck {
|
||||
id: string;
|
||||
target: string;
|
||||
targetType: string;
|
||||
status: 'HEALTHY' | 'WARNING' | 'CRITICAL' | 'UNKNOWN';
|
||||
responseTime: number;
|
||||
lastChecked: string;
|
||||
uptime: number;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
interface SystemMetric {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
threshold: number;
|
||||
status: 'NORMAL' | 'WARNING' | 'CRITICAL';
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
interface AlertItem {
|
||||
id: string;
|
||||
title: string;
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
status: 'TRIGGERED' | 'ACKNOWLEDGED' | 'RESOLVED';
|
||||
target: string;
|
||||
triggeredAt: string;
|
||||
description: string;
|
||||
value: number;
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
const MonitoringDashboard: React.FC<MonitoringDashboardProps> = ({ onNavigateToModule }) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [stats, setStats] = useState<MonitoringStats>({
|
||||
totalTargets: 0,
|
||||
healthyTargets: 0,
|
||||
warningTargets: 0,
|
||||
criticalTargets: 0,
|
||||
systemHealth: 0,
|
||||
avgResponseTime: 0,
|
||||
activeAlerts: 0,
|
||||
resolvedAlerts: 0,
|
||||
dataRetention: 0,
|
||||
lastHealthCheck: '',
|
||||
});
|
||||
const [healthChecks, setHealthChecks] = useState<HealthCheck[]>([]);
|
||||
const [systemMetrics, setSystemMetrics] = useState<SystemMetric[]>([]);
|
||||
const [alertList, setAlertList] = useState<AlertItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadMonitoringData();
|
||||
}, []);
|
||||
|
||||
const loadMonitoringData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Mock data - replace with actual API calls
|
||||
setStats({
|
||||
totalTargets: 45,
|
||||
healthyTargets: 38,
|
||||
warningTargets: 5,
|
||||
criticalTargets: 2,
|
||||
systemHealth: 87,
|
||||
avgResponseTime: 245,
|
||||
activeAlerts: 8,
|
||||
resolvedAlerts: 23,
|
||||
dataRetention: 89,
|
||||
lastHealthCheck: '2024-01-15T10:30:00Z',
|
||||
});
|
||||
|
||||
setHealthChecks([
|
||||
{
|
||||
id: '1',
|
||||
target: 'API Gateway',
|
||||
targetType: 'APPLICATION',
|
||||
status: 'HEALTHY',
|
||||
responseTime: 120,
|
||||
lastChecked: '2024-01-15T10:29:00Z',
|
||||
uptime: 99.9,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
target: 'Database Primary',
|
||||
targetType: 'DATABASE',
|
||||
status: 'WARNING',
|
||||
responseTime: 450,
|
||||
lastChecked: '2024-01-15T10:29:00Z',
|
||||
uptime: 99.5,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
target: 'Redis Cache',
|
||||
targetType: 'CACHE',
|
||||
status: 'HEALTHY',
|
||||
responseTime: 15,
|
||||
lastChecked: '2024-01-15T10:29:00Z',
|
||||
uptime: 99.8,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
target: 'Message Queue',
|
||||
targetType: 'QUEUE',
|
||||
status: 'CRITICAL',
|
||||
responseTime: 0,
|
||||
lastChecked: '2024-01-15T10:29:00Z',
|
||||
uptime: 85.2,
|
||||
errorMessage: 'Connection timeout',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
target: 'External API',
|
||||
targetType: 'EXTERNAL_API',
|
||||
status: 'HEALTHY',
|
||||
responseTime: 320,
|
||||
lastChecked: '2024-01-15T10:29:00Z',
|
||||
uptime: 99.7,
|
||||
},
|
||||
]);
|
||||
|
||||
setSystemMetrics([
|
||||
{
|
||||
id: '1',
|
||||
name: 'CPU Usage',
|
||||
category: 'SYSTEM_RESOURCES',
|
||||
value: 68,
|
||||
unit: '%',
|
||||
threshold: 80,
|
||||
status: 'NORMAL',
|
||||
trend: 'up',
|
||||
lastUpdated: '2024-01-15T10:30:00Z',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Memory Usage',
|
||||
category: 'SYSTEM_RESOURCES',
|
||||
value: 82,
|
||||
unit: '%',
|
||||
threshold: 85,
|
||||
status: 'WARNING',
|
||||
trend: 'up',
|
||||
lastUpdated: '2024-01-15T10:30:00Z',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Disk Usage',
|
||||
category: 'SYSTEM_RESOURCES',
|
||||
value: 45,
|
||||
unit: '%',
|
||||
threshold: 90,
|
||||
status: 'NORMAL',
|
||||
trend: 'stable',
|
||||
lastUpdated: '2024-01-15T10:30:00Z',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'API Response Time',
|
||||
category: 'API_RESPONSE_TIME',
|
||||
value: 245,
|
||||
unit: 'ms',
|
||||
threshold: 500,
|
||||
status: 'NORMAL',
|
||||
trend: 'down',
|
||||
lastUpdated: '2024-01-15T10:30:00Z',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Error Rate',
|
||||
category: 'ERROR_RATE',
|
||||
value: 0.2,
|
||||
unit: '%',
|
||||
threshold: 1,
|
||||
status: 'NORMAL',
|
||||
trend: 'down',
|
||||
lastUpdated: '2024-01-15T10:30:00Z',
|
||||
},
|
||||
]);
|
||||
|
||||
setAlertList([
|
||||
{
|
||||
id: '1',
|
||||
title: 'High Memory Usage',
|
||||
severity: 'HIGH',
|
||||
status: 'TRIGGERED',
|
||||
target: 'Database Primary',
|
||||
triggeredAt: '2024-01-15T10:25:00Z',
|
||||
description: 'Memory usage has exceeded 80% threshold',
|
||||
value: 82,
|
||||
threshold: 80,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Queue Connection Failed',
|
||||
severity: 'CRITICAL',
|
||||
status: 'TRIGGERED',
|
||||
target: 'Message Queue',
|
||||
triggeredAt: '2024-01-15T10:20:00Z',
|
||||
description: 'Unable to connect to message queue',
|
||||
value: 0,
|
||||
threshold: 1,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Slow API Response',
|
||||
severity: 'MEDIUM',
|
||||
status: 'ACKNOWLEDGED',
|
||||
target: 'API Gateway',
|
||||
triggeredAt: '2024-01-15T09:45:00Z',
|
||||
description: 'API response time above normal threshold',
|
||||
value: 650,
|
||||
threshold: 500,
|
||||
},
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load monitoring data:', error);
|
||||
setError('Failed to load monitoring data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'HEALTHY':
|
||||
case 'NORMAL':
|
||||
case 'RESOLVED':
|
||||
return 'success';
|
||||
case 'WARNING':
|
||||
case 'ACKNOWLEDGED':
|
||||
return 'warning';
|
||||
case 'CRITICAL':
|
||||
case 'TRIGGERED':
|
||||
return 'error';
|
||||
case 'UNKNOWN':
|
||||
return 'default';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'CRITICAL':
|
||||
return 'error';
|
||||
case 'HIGH':
|
||||
return 'warning';
|
||||
case 'MEDIUM':
|
||||
return 'info';
|
||||
case 'LOW':
|
||||
return 'success';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getTargetIcon = (targetType: string) => {
|
||||
switch (targetType) {
|
||||
case 'APPLICATION':
|
||||
return <ComputerIcon />;
|
||||
case 'DATABASE':
|
||||
return <DatabaseIcon />;
|
||||
case 'CACHE':
|
||||
return <MemoryIcon />;
|
||||
case 'QUEUE':
|
||||
return <QueueIcon />;
|
||||
case 'EXTERNAL_API':
|
||||
return <CloudIcon />;
|
||||
case 'INFRASTRUCTURE':
|
||||
return <MonitorIcon />;
|
||||
default:
|
||||
return <MonitorIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
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;
|
||||
subtitle?: string;
|
||||
}> = ({ title, value, icon, color, trend, trendValue, subtitle }) => (
|
||||
<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>
|
||||
{subtitle && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{subtitle}
|
||||
</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 Monitoring Dashboard...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" action={
|
||||
<Button color="inherit" size="small" onClick={loadMonitoringData}>
|
||||
Retry
|
||||
</Button>
|
||||
}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header */}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
System Monitoring
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
Real-time system health monitoring and alerting
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<IconButton onClick={loadMonitoringData}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* System Health Alert */}
|
||||
<Alert
|
||||
severity={stats.systemHealth > 90 ? 'success' : stats.systemHealth > 70 ? 'warning' : 'error'}
|
||||
sx={{ mb: 3 }}
|
||||
icon={stats.systemHealth > 90 ? <CheckCircleIcon /> : <WarningIcon />}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
System Health: {stats.systemHealth}%
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Overall system health is {stats.systemHealth > 90 ? 'excellent' : stats.systemHealth > 70 ? 'good' : 'degraded'}.
|
||||
{stats.criticalTargets > 0 && ` ${stats.criticalTargets} critical issues require immediate attention.`}
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
{/* Tabs */}
|
||||
<Paper sx={{ mb: 3 }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange} indicatorColor="primary" textColor="primary">
|
||||
<Tab label="Overview" />
|
||||
<Tab label="Health Checks" />
|
||||
<Tab label="System Metrics" />
|
||||
<Tab label="Alerts" />
|
||||
</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 Targets"
|
||||
value={stats.totalTargets}
|
||||
icon={<MonitorIcon />}
|
||||
color="primary"
|
||||
trend="up"
|
||||
trendValue="+2 this week"
|
||||
subtitle={`${stats.healthyTargets} healthy`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="System Health"
|
||||
value={`${stats.systemHealth}%`}
|
||||
icon={<CheckCircleIcon />}
|
||||
color="success"
|
||||
trend="up"
|
||||
trendValue="+3% this hour"
|
||||
subtitle="Overall health score"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Avg Response Time"
|
||||
value={`${stats.avgResponseTime}ms`}
|
||||
icon={<SpeedIcon />}
|
||||
color="info"
|
||||
trend="down"
|
||||
trendValue="-15ms from yesterday"
|
||||
subtitle="Across all targets"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Active Alerts"
|
||||
value={stats.activeAlerts}
|
||||
icon={<WarningIcon />}
|
||||
color="warning"
|
||||
trend="down"
|
||||
trendValue="-3 from yesterday"
|
||||
subtitle={`${stats.resolvedAlerts} resolved today`}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Health Status Overview */}
|
||||
<Grid container spacing={3} mb={3}>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Target Status Distribution
|
||||
</Typography>
|
||||
<Box mb={2}>
|
||||
<Box display="flex" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="body2">Healthy</Typography>
|
||||
<Typography variant="body2">{stats.healthyTargets}</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={(stats.healthyTargets / stats.totalTargets) * 100}
|
||||
color="success"
|
||||
sx={{ height: 8, borderRadius: 4 }}
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<Box display="flex" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="body2">Warning</Typography>
|
||||
<Typography variant="body2">{stats.warningTargets}</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={(stats.warningTargets / stats.totalTargets) * 100}
|
||||
color="warning"
|
||||
sx={{ height: 8, borderRadius: 4 }}
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<Box display="flex" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="body2">Critical</Typography>
|
||||
<Typography variant="body2">{stats.criticalTargets}</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={(stats.criticalTargets / stats.totalTargets) * 100}
|
||||
color="error"
|
||||
sx={{ height: 8, borderRadius: 4 }}
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Recent Health Checks
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Target</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Response Time</TableCell>
|
||||
<TableCell>Uptime</TableCell>
|
||||
<TableCell>Last Checked</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{healthChecks.slice(0, 5).map((check) => (
|
||||
<TableRow key={check.id}>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
{getTargetIcon(check.targetType)}
|
||||
<Typography variant="body2" sx={{ ml: 1 }}>
|
||||
{check.target}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={check.targetType} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={check.status}
|
||||
color={getStatusColor(check.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{check.responseTime}ms
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{check.uptime}%
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(check.lastChecked)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Health Checks Tab */}
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Health Checks & Target Monitoring
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Detailed monitoring status for all system targets
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Target</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Response Time</TableCell>
|
||||
<TableCell>Uptime</TableCell>
|
||||
<TableCell>Last Checked</TableCell>
|
||||
<TableCell>Error Message</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{healthChecks.map((check) => (
|
||||
<TableRow key={check.id}>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
{getTargetIcon(check.targetType)}
|
||||
<Typography variant="body2" sx={{ ml: 1 }}>
|
||||
{check.target}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={check.targetType} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={check.status}
|
||||
color={getStatusColor(check.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{check.responseTime}ms
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{check.uptime}%
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(check.lastChecked)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" color="error">
|
||||
{check.errorMessage || '-'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Configure
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* System Metrics Tab */}
|
||||
{activeTab === 2 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
System Metrics & Performance
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Real-time system performance metrics and trends
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3} sx={{ mt: 2 }}>
|
||||
{systemMetrics.map((metric) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={metric.id}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="h6">
|
||||
{metric.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={metric.status}
|
||||
color={getStatusColor(metric.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="h4" component="div" color="primary">
|
||||
{metric.value}{metric.unit}
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" mt={1}>
|
||||
{metric.trend === 'up' ? (
|
||||
<TrendingUpIcon color="error" fontSize="small" />
|
||||
) : metric.trend === 'down' ? (
|
||||
<TrendingDownIcon color="success" fontSize="small" />
|
||||
) : null}
|
||||
<Typography variant="caption" color="textSecondary" sx={{ ml: 0.5 }}>
|
||||
{metric.trend === 'up' ? 'Increasing' : metric.trend === 'down' ? 'Decreasing' : 'Stable'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box mt={2}>
|
||||
<Box display="flex" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="caption">Threshold</Typography>
|
||||
<Typography variant="caption">{metric.threshold}{metric.unit}</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={(metric.value / metric.threshold) * 100}
|
||||
color={metric.status === 'CRITICAL' ? 'error' : metric.status === 'WARNING' ? 'warning' : 'success'}
|
||||
sx={{ height: 6, borderRadius: 3 }}
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Alerts Tab */}
|
||||
{activeTab === 3 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Monitoring Alerts
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Active alerts and alert management
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Alert</TableCell>
|
||||
<TableCell>Severity</TableCell>
|
||||
<TableCell>Target</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Value</TableCell>
|
||||
<TableCell>Threshold</TableCell>
|
||||
<TableCell>Triggered At</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{alertList.map((alert) => (
|
||||
<TableRow key={alert.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{alert.title}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{alert.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={alert.severity}
|
||||
color={getSeverityColor(alert.severity) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{alert.target}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={alert.status}
|
||||
color={getStatusColor(alert.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{alert.value}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{alert.threshold}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(alert.triggeredAt)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Acknowledge
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitoringDashboard;
|
||||
585
etb-dashboard/src/components/Dashboard/OverviewDashboard.tsx
Normal file
585
etb-dashboard/src/components/Dashboard/OverviewDashboard.tsx
Normal file
@@ -0,0 +1,585 @@
|
||||
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;
|
||||
877
etb-dashboard/src/components/Dashboard/SLAOnCallDashboard.tsx
Normal file
877
etb-dashboard/src/components/Dashboard/SLAOnCallDashboard.tsx
Normal file
@@ -0,0 +1,877 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Grid,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Button,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Avatar,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Schedule as ScheduleIcon,
|
||||
Person as PersonIcon,
|
||||
Warning as WarningIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Settings as SettingsIcon,
|
||||
AccessTime as AccessTimeIcon,
|
||||
Timer as TimerIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
Phone as PhoneIcon,
|
||||
Email as EmailIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface SLAOnCallDashboardProps {
|
||||
onNavigateToModule: (moduleId: string) => void;
|
||||
}
|
||||
|
||||
interface SLAStats {
|
||||
totalSLAs: number;
|
||||
metSLAs: number;
|
||||
breachedSLAs: number;
|
||||
avgResponseTime: number;
|
||||
avgResolutionTime: number;
|
||||
complianceRate: number;
|
||||
escalationCount: number;
|
||||
businessHoursCompliance: number;
|
||||
}
|
||||
|
||||
interface OnCallStats {
|
||||
totalRotations: number;
|
||||
activeAssignments: number;
|
||||
upcomingHandoffs: number;
|
||||
avgResponseTime: number;
|
||||
incidentsHandled: number;
|
||||
availabilityRate: number;
|
||||
}
|
||||
|
||||
interface SLAInstance {
|
||||
id: string;
|
||||
incidentId: string;
|
||||
incidentTitle: string;
|
||||
slaType: string;
|
||||
targetTime: string;
|
||||
status: 'ACTIVE' | 'MET' | 'BREACHED';
|
||||
timeRemaining: number;
|
||||
progress: number;
|
||||
escalationLevel: number;
|
||||
assignedTo: string;
|
||||
}
|
||||
|
||||
interface OnCallAssignment {
|
||||
id: string;
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
};
|
||||
rotation: {
|
||||
name: string;
|
||||
teamName: string;
|
||||
};
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
status: 'ACTIVE' | 'UPCOMING' | 'COMPLETED';
|
||||
incidentsHandled: number;
|
||||
responseTime: number;
|
||||
}
|
||||
|
||||
interface EscalationEvent {
|
||||
id: string;
|
||||
incidentId: string;
|
||||
incidentTitle: string;
|
||||
escalationLevel: number;
|
||||
triggeredAt: string;
|
||||
status: 'PENDING' | 'TRIGGERED' | 'ACKNOWLEDGED' | 'RESOLVED';
|
||||
reason: string;
|
||||
notifiedUsers: string[];
|
||||
}
|
||||
|
||||
const SLAOnCallDashboard: React.FC<SLAOnCallDashboardProps> = ({ onNavigateToModule }) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [slaStats, setSlaStats] = useState<SLAStats>({
|
||||
totalSLAs: 0,
|
||||
metSLAs: 0,
|
||||
breachedSLAs: 0,
|
||||
avgResponseTime: 0,
|
||||
avgResolutionTime: 0,
|
||||
complianceRate: 0,
|
||||
escalationCount: 0,
|
||||
businessHoursCompliance: 0,
|
||||
});
|
||||
const [onCallStats, setOnCallStats] = useState<OnCallStats>({
|
||||
totalRotations: 0,
|
||||
activeAssignments: 0,
|
||||
upcomingHandoffs: 0,
|
||||
avgResponseTime: 0,
|
||||
incidentsHandled: 0,
|
||||
availabilityRate: 0,
|
||||
});
|
||||
const [slaInstances, setSlaInstances] = useState<SLAInstance[]>([]);
|
||||
const [onCallAssignments, setOnCallAssignments] = useState<OnCallAssignment[]>([]);
|
||||
const [escalationEvents, setEscalationEvents] = useState<EscalationEvent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadSLAOnCallData();
|
||||
}, []);
|
||||
|
||||
const loadSLAOnCallData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Mock data - replace with actual API calls
|
||||
setSlaStats({
|
||||
totalSLAs: 156,
|
||||
metSLAs: 142,
|
||||
breachedSLAs: 14,
|
||||
avgResponseTime: 0.8,
|
||||
avgResolutionTime: 2.5,
|
||||
complianceRate: 91,
|
||||
escalationCount: 8,
|
||||
businessHoursCompliance: 94,
|
||||
});
|
||||
|
||||
setOnCallStats({
|
||||
totalRotations: 12,
|
||||
activeAssignments: 8,
|
||||
upcomingHandoffs: 3,
|
||||
avgResponseTime: 0.5,
|
||||
incidentsHandled: 45,
|
||||
availabilityRate: 98,
|
||||
});
|
||||
|
||||
setSlaInstances([
|
||||
{
|
||||
id: '1',
|
||||
incidentId: 'INC-001',
|
||||
incidentTitle: 'Database Connection Timeout',
|
||||
slaType: 'Response Time',
|
||||
targetTime: '2024-01-15T11:30:00Z',
|
||||
status: 'ACTIVE',
|
||||
timeRemaining: 45,
|
||||
progress: 75,
|
||||
escalationLevel: 0,
|
||||
assignedTo: 'John Doe',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
incidentId: 'INC-002',
|
||||
incidentTitle: 'API Gateway Error',
|
||||
slaType: 'Resolution Time',
|
||||
targetTime: '2024-01-15T12:00:00Z',
|
||||
status: 'BREACHED',
|
||||
timeRemaining: -15,
|
||||
progress: 100,
|
||||
escalationLevel: 2,
|
||||
assignedTo: 'Jane Smith',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
incidentId: 'INC-003',
|
||||
incidentTitle: 'Cache Service Down',
|
||||
slaType: 'First Response',
|
||||
targetTime: '2024-01-15T11:45:00Z',
|
||||
status: 'MET',
|
||||
timeRemaining: 0,
|
||||
progress: 100,
|
||||
escalationLevel: 0,
|
||||
assignedTo: 'Mike Johnson',
|
||||
},
|
||||
]);
|
||||
|
||||
setOnCallAssignments([
|
||||
{
|
||||
id: '1',
|
||||
user: {
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@company.com',
|
||||
phone: '+1-555-0123',
|
||||
},
|
||||
rotation: {
|
||||
name: 'Primary On-Call',
|
||||
teamName: 'Platform Engineering',
|
||||
},
|
||||
startTime: '2024-01-15T08:00:00Z',
|
||||
endTime: '2024-01-16T08:00:00Z',
|
||||
status: 'ACTIVE',
|
||||
incidentsHandled: 3,
|
||||
responseTime: 0.3,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
user: {
|
||||
name: 'Jane Smith',
|
||||
email: 'jane.smith@company.com',
|
||||
phone: '+1-555-0124',
|
||||
},
|
||||
rotation: {
|
||||
name: 'Secondary On-Call',
|
||||
teamName: 'Platform Engineering',
|
||||
},
|
||||
startTime: '2024-01-16T08:00:00Z',
|
||||
endTime: '2024-01-17T08:00:00Z',
|
||||
status: 'UPCOMING',
|
||||
incidentsHandled: 0,
|
||||
responseTime: 0,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
user: {
|
||||
name: 'Mike Johnson',
|
||||
email: 'mike.johnson@company.com',
|
||||
phone: '+1-555-0125',
|
||||
},
|
||||
rotation: {
|
||||
name: 'Database Team',
|
||||
teamName: 'Database Operations',
|
||||
},
|
||||
startTime: '2024-01-15T06:00:00Z',
|
||||
endTime: '2024-01-15T18:00:00Z',
|
||||
status: 'ACTIVE',
|
||||
incidentsHandled: 2,
|
||||
responseTime: 0.7,
|
||||
},
|
||||
]);
|
||||
|
||||
setEscalationEvents([
|
||||
{
|
||||
id: '1',
|
||||
incidentId: 'INC-002',
|
||||
incidentTitle: 'API Gateway Error',
|
||||
escalationLevel: 2,
|
||||
triggeredAt: '2024-01-15T10:45:00Z',
|
||||
status: 'TRIGGERED',
|
||||
reason: 'SLA breach threshold exceeded',
|
||||
notifiedUsers: ['team-lead@company.com', 'manager@company.com'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
incidentId: 'INC-004',
|
||||
incidentTitle: 'Critical System Failure',
|
||||
escalationLevel: 3,
|
||||
triggeredAt: '2024-01-15T09:30:00Z',
|
||||
status: 'ACKNOWLEDGED',
|
||||
reason: 'No response within 15 minutes',
|
||||
notifiedUsers: ['director@company.com', 'cto@company.com'],
|
||||
},
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load SLA & On-Call data:', error);
|
||||
setError('Failed to load SLA & On-Call data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'MET':
|
||||
case 'COMPLETED':
|
||||
case 'RESOLVED':
|
||||
case 'ACTIVE':
|
||||
return 'success';
|
||||
case 'BREACHED':
|
||||
case 'TRIGGERED':
|
||||
return 'error';
|
||||
case 'PENDING':
|
||||
case 'UPCOMING':
|
||||
return 'warning';
|
||||
case 'ACKNOWLEDGED':
|
||||
return 'info';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
const formatDuration = (minutes: number) => {
|
||||
if (minutes < 0) {
|
||||
return `Overdue by ${Math.abs(minutes)}m`;
|
||||
}
|
||||
return `${minutes}m remaining`;
|
||||
};
|
||||
|
||||
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;
|
||||
subtitle?: string;
|
||||
}> = ({ title, value, icon, color, trend, trendValue, subtitle }) => (
|
||||
<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>
|
||||
{subtitle && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{subtitle}
|
||||
</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 SLA & On-Call Dashboard...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" action={
|
||||
<Button color="inherit" size="small" onClick={loadSLAOnCallData}>
|
||||
Retry
|
||||
</Button>
|
||||
}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header */}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
SLA & On-Call Management
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
Service level agreement monitoring and on-call rotation management
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<IconButton onClick={loadSLAOnCallData}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* SLA Compliance Alert */}
|
||||
<Alert
|
||||
severity={slaStats.complianceRate > 95 ? 'success' : slaStats.complianceRate > 85 ? 'warning' : 'error'}
|
||||
sx={{ mb: 3 }}
|
||||
icon={slaStats.complianceRate > 95 ? <CheckCircleIcon /> : <WarningIcon />}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
SLA Compliance: {slaStats.complianceRate}%
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{slaStats.complianceRate > 95 ? 'Excellent' : slaStats.complianceRate > 85 ? 'Good' : 'Needs improvement'} SLA compliance rate.
|
||||
{slaStats.breachedSLAs > 0 && ` ${slaStats.breachedSLAs} SLAs currently breached.`}
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
{/* Tabs */}
|
||||
<Paper sx={{ mb: 3 }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange} indicatorColor="primary" textColor="primary">
|
||||
<Tab label="Overview" />
|
||||
<Tab label="SLA Monitoring" />
|
||||
<Tab label="On-Call Schedule" />
|
||||
<Tab label="Escalations" />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 0 && (
|
||||
<>
|
||||
{/* SLA Stats Cards */}
|
||||
<Typography variant="h5" gutterBottom>
|
||||
SLA Performance
|
||||
</Typography>
|
||||
<Grid container spacing={3} mb={3}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Total SLAs"
|
||||
value={slaStats.totalSLAs}
|
||||
icon={<ScheduleIcon />}
|
||||
color="primary"
|
||||
trend="up"
|
||||
trendValue="+12 this week"
|
||||
subtitle={`${slaStats.metSLAs} met, ${slaStats.breachedSLAs} breached`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Compliance Rate"
|
||||
value={`${slaStats.complianceRate}%`}
|
||||
icon={<CheckCircleIcon />}
|
||||
color="success"
|
||||
trend="up"
|
||||
trendValue="+2% this month"
|
||||
subtitle="SLA compliance score"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Avg Response Time"
|
||||
value={`${slaStats.avgResponseTime}h`}
|
||||
icon={<AccessTimeIcon />}
|
||||
color="info"
|
||||
trend="down"
|
||||
trendValue="-0.2h this week"
|
||||
subtitle="Time to first response"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Escalations"
|
||||
value={slaStats.escalationCount}
|
||||
icon={<WarningIcon />}
|
||||
color="warning"
|
||||
trend="down"
|
||||
trendValue="-3 this week"
|
||||
subtitle="Active escalations"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* On-Call Stats Cards */}
|
||||
<Typography variant="h5" gutterBottom sx={{ mt: 4 }}>
|
||||
On-Call Management
|
||||
</Typography>
|
||||
<Grid container spacing={3} mb={3}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Active Assignments"
|
||||
value={onCallStats.activeAssignments}
|
||||
icon={<PersonIcon />}
|
||||
color="primary"
|
||||
trend="neutral"
|
||||
trendValue="No change"
|
||||
subtitle={`${onCallStats.upcomingHandoffs} upcoming handoffs`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Availability Rate"
|
||||
value={`${onCallStats.availabilityRate}%`}
|
||||
icon={<CheckCircleIcon />}
|
||||
color="success"
|
||||
trend="up"
|
||||
trendValue="+1% this month"
|
||||
subtitle="On-call availability"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Incidents Handled"
|
||||
value={onCallStats.incidentsHandled}
|
||||
icon={<AssessmentIcon />}
|
||||
color="info"
|
||||
trend="up"
|
||||
trendValue="+8 this week"
|
||||
subtitle="This week's total"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Avg Response Time"
|
||||
value={`${onCallStats.avgResponseTime}h`}
|
||||
icon={<TimerIcon />}
|
||||
color="secondary"
|
||||
trend="down"
|
||||
trendValue="-0.1h this week"
|
||||
subtitle="On-call response time"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Current On-Call Status */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Current On-Call Assignments
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{onCallAssignments.filter(a => a.status === 'ACTIVE').map((assignment) => (
|
||||
<Grid size={{ xs: 12, md: 6 }} key={assignment.id}>
|
||||
<Paper sx={{ p: 2, border: '1px solid', borderColor: 'primary.main' }}>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<Avatar sx={{ mr: 2 }}>
|
||||
{assignment.user.name.split(' ').map(n => n[0]).join('')}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h6">
|
||||
{assignment.user.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{assignment.rotation.teamName}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="body2">
|
||||
<PhoneIcon fontSize="small" sx={{ mr: 0.5, verticalAlign: 'middle' }} />
|
||||
{assignment.user.phone}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<EmailIcon fontSize="small" sx={{ mr: 0.5, verticalAlign: 'middle' }} />
|
||||
{assignment.user.email}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Incidents: {assignment.incidentsHandled}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Avg Response: {assignment.responseTime}h
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* SLA Monitoring Tab */}
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
SLA Monitoring & Tracking
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Real-time SLA status and performance tracking
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Incident</TableCell>
|
||||
<TableCell>SLA Type</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Progress</TableCell>
|
||||
<TableCell>Time Remaining</TableCell>
|
||||
<TableCell>Escalation</TableCell>
|
||||
<TableCell>Assigned To</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{slaInstances.map((sla) => (
|
||||
<TableRow key={sla.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{sla.incidentId}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{sla.incidentTitle}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={sla.slaType} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={sla.status}
|
||||
color={getStatusColor(sla.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={sla.progress}
|
||||
color={sla.status === 'BREACHED' ? 'error' : sla.status === 'ACTIVE' ? 'primary' : 'success'}
|
||||
sx={{ width: 60, mr: 1 }}
|
||||
/>
|
||||
<Typography variant="caption">
|
||||
{sla.progress}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={sla.timeRemaining < 0 ? 'error' : sla.timeRemaining < 30 ? 'warning' : 'textPrimary'}
|
||||
>
|
||||
{formatDuration(sla.timeRemaining)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{sla.escalationLevel > 0 ? (
|
||||
<Chip
|
||||
label={`Level ${sla.escalationLevel}`}
|
||||
color="error"
|
||||
size="small"
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
None
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{sla.assignedTo}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
View Details
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* On-Call Schedule Tab */}
|
||||
{activeTab === 2 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
On-Call Schedule & Rotations
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Manage on-call rotations and assignments
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>On-Call Engineer</TableCell>
|
||||
<TableCell>Rotation</TableCell>
|
||||
<TableCell>Team</TableCell>
|
||||
<TableCell>Schedule</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Performance</TableCell>
|
||||
<TableCell>Contact</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{onCallAssignments.map((assignment) => (
|
||||
<TableRow key={assignment.id}>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Avatar sx={{ mr: 2 }}>
|
||||
{assignment.user.name.split(' ').map(n => n[0]).join('')}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{assignment.user.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{assignment.user.email}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{assignment.rotation.name}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{assignment.rotation.teamName}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(assignment.startTime)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
to {formatTime(assignment.endTime)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={assignment.status}
|
||||
color={getStatusColor(assignment.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box>
|
||||
<Typography variant="caption" display="block">
|
||||
Incidents: {assignment.incidentsHandled}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block">
|
||||
Response: {assignment.responseTime}h
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box>
|
||||
<Typography variant="caption" display="block">
|
||||
<PhoneIcon fontSize="small" sx={{ mr: 0.5, verticalAlign: 'middle' }} />
|
||||
{assignment.user.phone}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Manage
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Escalations Tab */}
|
||||
{activeTab === 3 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Escalation Management
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Track and manage incident escalations
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Incident</TableCell>
|
||||
<TableCell>Escalation Level</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Reason</TableCell>
|
||||
<TableCell>Notified Users</TableCell>
|
||||
<TableCell>Triggered At</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{escalationEvents.map((event) => (
|
||||
<TableRow key={event.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{event.incidentId}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{event.incidentTitle}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={`Level ${event.escalationLevel}`}
|
||||
color={event.escalationLevel >= 3 ? 'error' : event.escalationLevel >= 2 ? 'warning' : 'info'}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={event.status}
|
||||
color={getStatusColor(event.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{event.reason}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="caption">
|
||||
{event.notifiedUsers.length} users notified
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(event.triggeredAt)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Acknowledge
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SLAOnCallDashboard;
|
||||
842
etb-dashboard/src/components/Dashboard/SecurityDashboard.tsx
Normal file
842
etb-dashboard/src/components/Dashboard/SecurityDashboard.tsx
Normal file
@@ -0,0 +1,842 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Grid,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Button,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Avatar,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Tabs,
|
||||
Tab,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Shield as ShieldIcon,
|
||||
VpnKey as VpnKeyIcon,
|
||||
Person as PersonIcon,
|
||||
Warning as WarningIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Settings as SettingsIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface SecurityDashboardProps {
|
||||
onNavigateToModule: (moduleId: string) => void;
|
||||
}
|
||||
|
||||
interface SecurityStats {
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
mfaEnabled: number;
|
||||
failedLogins: number;
|
||||
securityEvents: number;
|
||||
complianceScore: number;
|
||||
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
lastSecurityScan: string;
|
||||
}
|
||||
|
||||
interface SecurityEvent {
|
||||
id: string;
|
||||
type: string;
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
description: string;
|
||||
user: string;
|
||||
timestamp: string;
|
||||
status: 'ACTIVE' | 'RESOLVED' | 'INVESTIGATING';
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface UserSecurity {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
clearanceLevel: number;
|
||||
lastLogin: string;
|
||||
mfaEnabled: boolean;
|
||||
riskScore: number;
|
||||
status: 'ACTIVE' | 'LOCKED' | 'SUSPENDED';
|
||||
failedAttempts: number;
|
||||
}
|
||||
|
||||
const SecurityDashboard: React.FC<SecurityDashboardProps> = ({ onNavigateToModule }) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [stats, setStats] = useState<SecurityStats>({
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
mfaEnabled: 0,
|
||||
failedLogins: 0,
|
||||
securityEvents: 0,
|
||||
complianceScore: 0,
|
||||
riskLevel: 'LOW',
|
||||
lastSecurityScan: '',
|
||||
});
|
||||
const [securityEvents, setSecurityEvents] = useState<SecurityEvent[]>([]);
|
||||
const [users, setUsers] = useState<UserSecurity[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadSecurityData();
|
||||
}, []);
|
||||
|
||||
const loadSecurityData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Mock data - replace with actual API calls
|
||||
setStats({
|
||||
totalUsers: 156,
|
||||
activeUsers: 142,
|
||||
mfaEnabled: 134,
|
||||
failedLogins: 23,
|
||||
securityEvents: 8,
|
||||
complianceScore: 94,
|
||||
riskLevel: 'LOW',
|
||||
lastSecurityScan: '2024-01-15T10:30:00Z',
|
||||
});
|
||||
|
||||
setSecurityEvents([
|
||||
{
|
||||
id: '1',
|
||||
type: 'Failed Login',
|
||||
severity: 'MEDIUM',
|
||||
description: 'Multiple failed login attempts detected',
|
||||
user: 'john.doe@company.com',
|
||||
timestamp: '2024-01-15T10:25:00Z',
|
||||
status: 'INVESTIGATING',
|
||||
source: '192.168.1.100',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'Privilege Escalation',
|
||||
severity: 'HIGH',
|
||||
description: 'Unusual privilege escalation attempt',
|
||||
user: 'admin@company.com',
|
||||
timestamp: '2024-01-15T09:45:00Z',
|
||||
status: 'ACTIVE',
|
||||
source: '10.0.0.50',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'Data Access',
|
||||
severity: 'LOW',
|
||||
description: 'Access to restricted data classification',
|
||||
user: 'analyst@company.com',
|
||||
timestamp: '2024-01-15T08:30:00Z',
|
||||
status: 'RESOLVED',
|
||||
source: '172.16.0.25',
|
||||
},
|
||||
]);
|
||||
|
||||
setUsers([
|
||||
{
|
||||
id: '1',
|
||||
username: 'john.doe',
|
||||
email: 'john.doe@company.com',
|
||||
clearanceLevel: 2,
|
||||
lastLogin: '2024-01-15T10:20:00Z',
|
||||
mfaEnabled: true,
|
||||
riskScore: 25,
|
||||
status: 'ACTIVE',
|
||||
failedAttempts: 0,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
username: 'jane.smith',
|
||||
email: 'jane.smith@company.com',
|
||||
clearanceLevel: 3,
|
||||
lastLogin: '2024-01-15T09:15:00Z',
|
||||
mfaEnabled: true,
|
||||
riskScore: 15,
|
||||
status: 'ACTIVE',
|
||||
failedAttempts: 0,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
username: 'admin',
|
||||
email: 'admin@company.com',
|
||||
clearanceLevel: 5,
|
||||
lastLogin: '2024-01-15T08:45:00Z',
|
||||
mfaEnabled: true,
|
||||
riskScore: 5,
|
||||
status: 'ACTIVE',
|
||||
failedAttempts: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load security data:', error);
|
||||
setError('Failed to load security data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'CRITICAL':
|
||||
return 'error';
|
||||
case 'HIGH':
|
||||
return 'warning';
|
||||
case 'MEDIUM':
|
||||
return 'info';
|
||||
case 'LOW':
|
||||
return 'success';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
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;
|
||||
subtitle?: string;
|
||||
}> = ({ title, value, icon, color, trend, trendValue, subtitle }) => (
|
||||
<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>
|
||||
{subtitle && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{subtitle}
|
||||
</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 Security Dashboard...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" action={
|
||||
<Button color="inherit" size="small" onClick={loadSecurityData}>
|
||||
Retry
|
||||
</Button>
|
||||
}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header */}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Security & Access Control
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
Zero Trust Architecture & Enterprise Security Management
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<IconButton onClick={loadSecurityData}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Security Status Alert */}
|
||||
<Alert
|
||||
severity={stats.riskLevel === 'LOW' ? 'success' : stats.riskLevel === 'MEDIUM' ? 'warning' : 'error'}
|
||||
sx={{ mb: 3 }}
|
||||
icon={stats.riskLevel === 'LOW' ? <CheckCircleIcon /> : <WarningIcon />}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
Security Status: {stats.riskLevel} RISK
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
System security posture is {stats.riskLevel.toLowerCase()}.
|
||||
{stats.securityEvents > 0 && ` ${stats.securityEvents} active security events require attention.`}
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
{/* Tabs */}
|
||||
<Paper sx={{ mb: 3 }}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange} indicatorColor="primary" textColor="primary">
|
||||
<Tab label="Overview" />
|
||||
<Tab label="Security Events" />
|
||||
<Tab label="User Management" />
|
||||
<Tab label="Compliance" />
|
||||
</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 Users"
|
||||
value={stats.totalUsers}
|
||||
icon={<PersonIcon />}
|
||||
color="primary"
|
||||
trend="up"
|
||||
trendValue="+3 this week"
|
||||
subtitle={`${stats.activeUsers} active`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="MFA Enabled"
|
||||
value={stats.mfaEnabled}
|
||||
icon={<VpnKeyIcon />}
|
||||
color="success"
|
||||
trend="up"
|
||||
trendValue="+5 this week"
|
||||
subtitle={`${Math.round((stats.mfaEnabled / stats.totalUsers) * 100)}% coverage`}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Security Events"
|
||||
value={stats.securityEvents}
|
||||
icon={<WarningIcon />}
|
||||
color="warning"
|
||||
trend="down"
|
||||
trendValue="-2 from yesterday"
|
||||
subtitle="Active incidents"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Compliance Score"
|
||||
value={`${stats.complianceScore}%`}
|
||||
icon={<ShieldIcon />}
|
||||
color="info"
|
||||
trend="up"
|
||||
trendValue="+1% this month"
|
||||
subtitle="SOX, HIPAA, GDPR"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Security Features */}
|
||||
<Grid container spacing={3} mb={3}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Zero Trust Status
|
||||
</Typography>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Device Posture Assessment"
|
||||
secondary="All devices verified and compliant"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Adaptive Authentication"
|
||||
secondary="Risk-based MFA active"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Behavioral Analysis"
|
||||
secondary="User behavior monitoring active"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<WarningIcon color="warning" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Geolocation Rules"
|
||||
secondary="2 unusual location accesses"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Security Controls
|
||||
</Typography>
|
||||
<Box mb={2}>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={true} />}
|
||||
label="Zero Trust Enforcement"
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={true} />}
|
||||
label="Multi-Factor Authentication"
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={true} />}
|
||||
label="Device Registration"
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={false} />}
|
||||
label="Strict Mode (Beta)"
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => onNavigateToModule('security')}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Advanced Security Settings
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Recent Security Events */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Typography variant="h6">
|
||||
Recent Security Events
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => setActiveTab(1)}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</Box>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Event Type</TableCell>
|
||||
<TableCell>Severity</TableCell>
|
||||
<TableCell>User</TableCell>
|
||||
<TableCell>Source IP</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Timestamp</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{securityEvents.slice(0, 5).map((event) => (
|
||||
<TableRow key={event.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{event.type}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={event.severity}
|
||||
color={getSeverityColor(event.severity) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{event.user}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontFamily="monospace">
|
||||
{event.source}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={event.status}
|
||||
color={event.status === 'RESOLVED' ? 'success' : event.status === 'ACTIVE' ? 'error' : 'warning'}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(event.timestamp)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Security Events Tab */}
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Security Events & Incidents
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Real-time security monitoring and threat detection
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Event Type</TableCell>
|
||||
<TableCell>Severity</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell>User</TableCell>
|
||||
<TableCell>Source IP</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Timestamp</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{securityEvents.map((event) => (
|
||||
<TableRow key={event.id}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{event.type}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={event.severity}
|
||||
color={getSeverityColor(event.severity) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{event.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{event.user}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontFamily="monospace">
|
||||
{event.source}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={event.status}
|
||||
color={event.status === 'RESOLVED' ? 'success' : event.status === 'ACTIVE' ? 'error' : 'warning'}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(event.timestamp)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Investigate
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* User Management Tab */}
|
||||
{activeTab === 2 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
User Security Management
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
User access control and security posture management
|
||||
</Typography>
|
||||
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>User</TableCell>
|
||||
<TableCell>Email</TableCell>
|
||||
<TableCell>Clearance Level</TableCell>
|
||||
<TableCell>MFA Status</TableCell>
|
||||
<TableCell>Risk Score</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Last Login</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Avatar sx={{ width: 32, height: 32, mr: 2 }}>
|
||||
{user.username[0].toUpperCase()}
|
||||
</Avatar>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
{user.username}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{user.email}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={`Level ${user.clearanceLevel}`}
|
||||
color="info"
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
{user.mfaEnabled ? (
|
||||
<CheckCircleIcon color="success" fontSize="small" />
|
||||
) : (
|
||||
<ErrorIcon color="error" fontSize="small" />
|
||||
)}
|
||||
<Typography variant="body2" sx={{ ml: 1 }}>
|
||||
{user.mfaEnabled ? 'Enabled' : 'Disabled'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={user.riskScore}
|
||||
color={user.riskScore > 50 ? 'error' : user.riskScore > 25 ? 'warning' : 'success'}
|
||||
sx={{ width: 60, mr: 1 }}
|
||||
/>
|
||||
<Typography variant="body2">
|
||||
{user.riskScore}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={user.status}
|
||||
color={user.status === 'ACTIVE' ? 'success' : user.status === 'LOCKED' ? 'error' : 'warning'}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{formatTime(user.lastLogin)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Manage
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Compliance Tab */}
|
||||
{activeTab === 3 && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Compliance & Governance
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Regulatory compliance and governance framework
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3} sx={{ mt: 2 }}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Compliance Frameworks
|
||||
</Typography>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="SOX Compliance"
|
||||
secondary="94% compliant - 3 items pending"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="HIPAA Compliance"
|
||||
secondary="96% compliant - 2 items pending"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="GDPR Compliance"
|
||||
secondary="92% compliant - 5 items pending"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="PCI-DSS Compliance"
|
||||
secondary="98% compliant - 1 item pending"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Audit Trail
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" paragraph>
|
||||
Immutable audit trail captures all security events and access patterns.
|
||||
Current retention period: 7 years (SOX compliant).
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Total Audit Records"
|
||||
secondary="2,847,392 entries"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Last Audit Report"
|
||||
secondary="Generated 2 days ago"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Compliance Score"
|
||||
secondary={`${stats.complianceScore}% overall`}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => onNavigateToModule('compliance_governance')}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
View Compliance Dashboard
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecurityDashboard;
|
||||
@@ -0,0 +1,485 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Chip,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
OutlinedInput,
|
||||
Checkbox,
|
||||
ListItemText,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Tabs,
|
||||
Tab,
|
||||
Avatar,
|
||||
Tooltip,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Edit as EditIcon,
|
||||
AdminPanelSettings as AdminIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Cancel as CancelIcon,
|
||||
Save as SaveIcon,
|
||||
Refresh as RefreshIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import apiService from '../../services/api';
|
||||
import { User } from '../../types';
|
||||
|
||||
interface UserManagementDashboardProps {
|
||||
onNavigateToModule?: (moduleId: string) => void;
|
||||
}
|
||||
|
||||
interface DashboardComponent {
|
||||
name: string;
|
||||
permissions: string[];
|
||||
clearance_level?: number;
|
||||
}
|
||||
|
||||
const UserManagementDashboard: React.FC<UserManagementDashboardProps> = ({ onNavigateToModule }) => {
|
||||
const { user: currentUser } = useAuth();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [roles, setRoles] = useState<any[]>([]);
|
||||
const [classifications, setClassifications] = useState<any[]>([]);
|
||||
const [dashboardComponents, setDashboardComponents] = useState<Record<string, DashboardComponent>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [selectedRoles, setSelectedRoles] = useState<string[]>([]);
|
||||
const [selectedClearance, setSelectedClearance] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [userAccessAnalysis, setUserAccessAnalysis] = useState<Record<string, any>>({});
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const [usersData, rolesData, classificationsData, componentsData] = await Promise.all([
|
||||
apiService.getUsers(),
|
||||
apiService.getRoles(),
|
||||
apiService.getDataClassifications(),
|
||||
apiService.getDashboardPermissions(),
|
||||
]);
|
||||
|
||||
// Handle paginated response
|
||||
const usersList = Array.isArray(usersData) ? usersData : (usersData as any).results || [];
|
||||
setUsers(usersList);
|
||||
setRoles(rolesData);
|
||||
setClassifications(classificationsData);
|
||||
setDashboardComponents(componentsData);
|
||||
analyzeUserAccess(usersList, componentsData);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load user management data');
|
||||
console.error('Error loading data:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
const analyzeUserAccess = (usersList: User[], components: Record<string, DashboardComponent>) => {
|
||||
const analysis: Record<string, any> = {};
|
||||
|
||||
usersList.forEach(user => {
|
||||
const accessibleComponents: string[] = [];
|
||||
const deniedComponents: string[] = [];
|
||||
|
||||
Object.entries(components).forEach(([componentName, requirements]) => {
|
||||
let canAccess = true;
|
||||
const reasons: string[] = [];
|
||||
|
||||
// Superusers bypass all checks
|
||||
if (user.is_superuser || user.is_staff) {
|
||||
accessibleComponents.push(componentName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check clearance level
|
||||
if (requirements.clearance_level && user.clearance_level.level < requirements.clearance_level) {
|
||||
canAccess = false;
|
||||
reasons.push(`Insufficient clearance (has ${user.clearance_level.level}, needs ${requirements.clearance_level})`);
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if (requirements.permissions.length > 0) {
|
||||
const userPermissions = user.roles?.flatMap(role =>
|
||||
role.permissions.map(perm => perm.codename)
|
||||
) || [];
|
||||
const hasRequiredPermission = requirements.permissions.some(perm =>
|
||||
userPermissions.includes(perm)
|
||||
);
|
||||
if (!hasRequiredPermission) {
|
||||
canAccess = false;
|
||||
reasons.push(`Missing permissions: ${requirements.permissions.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (canAccess) {
|
||||
accessibleComponents.push(componentName);
|
||||
} else {
|
||||
deniedComponents.push(componentName);
|
||||
}
|
||||
});
|
||||
|
||||
analysis[user.id] = {
|
||||
accessible: accessibleComponents,
|
||||
denied: deniedComponents,
|
||||
totalAccessible: accessibleComponents.length,
|
||||
totalDenied: deniedComponents.length,
|
||||
};
|
||||
});
|
||||
|
||||
setUserAccessAnalysis(analysis);
|
||||
};
|
||||
|
||||
const handleEditUser = (user: User) => {
|
||||
setSelectedUser(user);
|
||||
setSelectedRoles(user.roles?.map(role => role.id) || []);
|
||||
setSelectedClearance((user.clearance_level as any)?.id || '');
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveUser = async () => {
|
||||
if (!selectedUser) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Update roles if changed
|
||||
if (JSON.stringify(selectedRoles.sort()) !== JSON.stringify((selectedUser.roles?.map(role => role.id) || []).sort())) {
|
||||
await apiService.updateUserRoles(selectedUser.id, selectedRoles);
|
||||
}
|
||||
|
||||
// Update clearance level if changed
|
||||
if (selectedClearance !== (selectedUser.clearance_level as any)?.id) {
|
||||
await apiService.updateUserClearance(selectedUser.id, selectedClearance);
|
||||
}
|
||||
|
||||
// Reload data to reflect changes
|
||||
await loadData();
|
||||
setEditDialogOpen(false);
|
||||
setSelectedUser(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to update user');
|
||||
console.error('Error updating user:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getClearanceColor = (level: number) => {
|
||||
switch (level) {
|
||||
case 1: return 'default';
|
||||
case 2: return 'primary';
|
||||
case 3: return 'secondary';
|
||||
case 4: return 'warning';
|
||||
case 5: return 'error';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getAccessColor = (accessible: number, total: number) => {
|
||||
const percentage = (accessible / total) * 100;
|
||||
if (percentage >= 80) return 'success';
|
||||
if (percentage >= 50) return 'warning';
|
||||
return 'error';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" action={
|
||||
<Button color="inherit" size="small" onClick={loadData}>
|
||||
Retry
|
||||
</Button>
|
||||
}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Typography variant="h4" component="h1">
|
||||
User Management
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={loadData}
|
||||
disabled={loading}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Tabs value={activeTab} onChange={(e, newValue) => setActiveTab(newValue)} sx={{ mb: 3 }}>
|
||||
<Tab label="User Overview" />
|
||||
<Tab label="Access Analysis" />
|
||||
</Tabs>
|
||||
|
||||
{activeTab === 0 && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>User</TableCell>
|
||||
<TableCell>Clearance Level</TableCell>
|
||||
<TableCell>Roles</TableCell>
|
||||
<TableCell>Access Level</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Avatar sx={{ mr: 2, bgcolor: 'primary.main' }}>
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="subtitle2">
|
||||
{user.first_name} {user.last_name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{user.username}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={`${user.clearance_level?.name || 'None'} (Level ${user.clearance_level?.level || 0})`}
|
||||
color={getClearanceColor(user.clearance_level?.level || 0) as any}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" flexWrap="wrap" gap={0.5}>
|
||||
{user.roles?.map((role) => (
|
||||
<Chip
|
||||
key={role.id}
|
||||
label={role.name}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
)) || <Typography variant="caption">No roles</Typography>}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Chip
|
||||
label={`${userAccessAnalysis[user.id]?.totalAccessible || 0}/${Object.keys(dashboardComponents).length} components`}
|
||||
color={getAccessColor(
|
||||
userAccessAnalysis[user.id]?.totalAccessible || 0,
|
||||
Object.keys(dashboardComponents).length
|
||||
) as any}
|
||||
size="small"
|
||||
/>
|
||||
{user.is_superuser && (
|
||||
<Tooltip title="Superuser - Full Access">
|
||||
<AdminIcon sx={{ ml: 1, color: 'success.main' }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" alignItems="center">
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={user.is_active}
|
||||
disabled
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={user.is_active ? 'Active' : 'Inactive'}
|
||||
/>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton
|
||||
onClick={() => handleEditUser(user)}
|
||||
disabled={user.id === currentUser?.id}
|
||||
size="small"
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
{activeTab === 1 && (
|
||||
<Grid container spacing={3}>
|
||||
{users.map((user) => (
|
||||
<Grid size={{ xs: 12, md: 6 }} key={user.id}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<Avatar sx={{ mr: 2, bgcolor: 'primary.main' }}>
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h6">
|
||||
{user.first_name} {user.last_name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{user.username}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Accessible Components ({userAccessAnalysis[user.id]?.totalAccessible || 0})
|
||||
</Typography>
|
||||
<Box display="flex" flexWrap="wrap" gap={0.5} mb={2}>
|
||||
{userAccessAnalysis[user.id]?.accessible?.map((component: string) => (
|
||||
<Chip
|
||||
key={component}
|
||||
label={component}
|
||||
size="small"
|
||||
color="success"
|
||||
icon={<CheckCircleIcon />}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{userAccessAnalysis[user.id]?.denied?.length > 0 && (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Denied Components ({userAccessAnalysis[user.id]?.totalDenied || 0})
|
||||
</Typography>
|
||||
<Box display="flex" flexWrap="wrap" gap={0.5}>
|
||||
{userAccessAnalysis[user.id]?.denied?.map((component: string) => (
|
||||
<Chip
|
||||
key={component}
|
||||
label={component}
|
||||
size="small"
|
||||
color="error"
|
||||
icon={<CancelIcon />}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Edit User Dialog */}
|
||||
<Dialog open={editDialogOpen} onClose={() => setEditDialogOpen(false)} maxWidth="md" fullWidth>
|
||||
<DialogTitle>
|
||||
Edit User: {selectedUser?.first_name} {selectedUser?.last_name}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={3} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Clearance Level</InputLabel>
|
||||
<Select
|
||||
value={selectedClearance}
|
||||
onChange={(e) => setSelectedClearance(e.target.value)}
|
||||
label="Clearance Level"
|
||||
>
|
||||
{classifications.map((classification) => (
|
||||
<MenuItem key={classification.id} value={classification.id}>
|
||||
{classification.name} (Level {classification.level})
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Roles</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={selectedRoles}
|
||||
onChange={(e) => setSelectedRoles(e.target.value as string[])}
|
||||
input={<OutlinedInput label="Roles" />}
|
||||
renderValue={(selected) => (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{selected.map((value) => {
|
||||
const role = roles.find(r => r.id === value);
|
||||
return (
|
||||
<Chip key={value} label={role?.name || value} size="small" />
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
{roles.map((role) => (
|
||||
<MenuItem key={role.id} value={role.id}>
|
||||
<Checkbox checked={selectedRoles.indexOf(role.id) > -1} />
|
||||
<ListItemText primary={role.name} secondary={role.description} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setEditDialogOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleSaveUser}
|
||||
variant="contained"
|
||||
startIcon={<SaveIcon />}
|
||||
disabled={loading}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManagementDashboard;
|
||||
157
etb-dashboard/src/components/Login/ETBLogo.tsx
Normal file
157
etb-dashboard/src/components/Login/ETBLogo.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Avatar } from '@mui/material';
|
||||
import { Security } from '@mui/icons-material';
|
||||
|
||||
interface ETBLogoProps {
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
showSubtitle?: boolean;
|
||||
variant?: 'horizontal' | 'vertical';
|
||||
}
|
||||
|
||||
const ETBLogo: React.FC<ETBLogoProps> = ({
|
||||
size = 'medium',
|
||||
showSubtitle = true,
|
||||
variant = 'horizontal'
|
||||
}) => {
|
||||
const getSizeConfig = () => {
|
||||
switch (size) {
|
||||
case 'small':
|
||||
return {
|
||||
avatarSize: 60,
|
||||
iconSize: 30,
|
||||
titleSize: 'h6',
|
||||
subtitleSize: 'caption',
|
||||
spacing: 1.5,
|
||||
};
|
||||
case 'large':
|
||||
return {
|
||||
avatarSize: 120,
|
||||
iconSize: 60,
|
||||
titleSize: 'h3',
|
||||
subtitleSize: 'subtitle1',
|
||||
spacing: 2,
|
||||
};
|
||||
default: // medium
|
||||
return {
|
||||
avatarSize: 100,
|
||||
iconSize: 50,
|
||||
titleSize: 'h4',
|
||||
subtitleSize: 'subtitle1',
|
||||
spacing: 2,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getSizeConfig();
|
||||
|
||||
const LogoContent = () => (
|
||||
<>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: config.avatarSize,
|
||||
height: config.avatarSize,
|
||||
background: 'linear-gradient(135deg, #1e3c72 0%, #2a5298 100%)',
|
||||
boxShadow: '0 8px 32px rgba(30, 60, 114, 0.3)',
|
||||
position: 'relative',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '60%',
|
||||
height: '60%',
|
||||
background: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)',
|
||||
borderRadius: '50%',
|
||||
opacity: 0.1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Security sx={{ fontSize: config.iconSize, color: 'white' }} />
|
||||
</Avatar>
|
||||
|
||||
<Box sx={{ ml: config.spacing }}>
|
||||
<Typography
|
||||
variant={config.titleSize as any}
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: '#ffffff',
|
||||
letterSpacing: '0.5px',
|
||||
lineHeight: 1.2,
|
||||
textShadow: '0 2px 4px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
ETB Security
|
||||
</Typography>
|
||||
{showSubtitle && (
|
||||
<Typography
|
||||
variant={config.subtitleSize as any}
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
mt: 0.5,
|
||||
opacity: 0.9,
|
||||
color: '#94a3b8', // slate-400 for dark theme
|
||||
fontSize: size === 'small' ? '0.75rem' : size === 'large' ? '1rem' : '0.875rem',
|
||||
}}
|
||||
>
|
||||
Enterprise Threat & Breach Management
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
if (variant === 'vertical') {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: config.avatarSize,
|
||||
height: config.avatarSize,
|
||||
background: 'linear-gradient(135deg, #1e3c72 0%, #2a5298 100%)',
|
||||
boxShadow: '0 8px 32px rgba(30, 60, 114, 0.3)',
|
||||
mb: config.spacing,
|
||||
}}
|
||||
>
|
||||
<Security sx={{ fontSize: config.iconSize, color: 'white' }} />
|
||||
</Avatar>
|
||||
<Typography
|
||||
variant={config.titleSize as any}
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: '#ffffff',
|
||||
letterSpacing: '0.5px',
|
||||
lineHeight: 1.2,
|
||||
textShadow: '0 2px 4px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
ETB Security
|
||||
</Typography>
|
||||
{showSubtitle && (
|
||||
<Typography
|
||||
variant={config.subtitleSize as any}
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
mt: 0.5,
|
||||
opacity: 0.9,
|
||||
color: '#94a3b8', // slate-400 for dark theme
|
||||
fontSize: size === 'small' ? '0.75rem' : size === 'large' ? '1rem' : '0.875rem',
|
||||
}}
|
||||
>
|
||||
Enterprise Threat & Breach Management
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<LogoContent />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ETBLogo;
|
||||
293
etb-dashboard/src/components/Login/LoginPage.css
Normal file
293
etb-dashboard/src/components/Login/LoginPage.css
Normal file
@@ -0,0 +1,293 @@
|
||||
/* ETB Security Enterprise Login Page Styles */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.etb-login-container {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.etb-gradient-bg {
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #667eea 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.etb-gradient-bg::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 119, 198, 0.2) 0%, transparent 50%);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.etb-glass-card {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
box-shadow:
|
||||
0 25px 50px rgba(0, 0, 0, 0.15),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.etb-logo-gradient {
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.etb-primary-button {
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
box-shadow: 0 4px 20px rgba(30, 60, 114, 0.3);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.etb-primary-button:hover {
|
||||
background: linear-gradient(135deg, #2a5298 0%, #1e3c72 100%);
|
||||
box-shadow: 0 6px 25px rgba(30, 60, 114, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.etb-success-button {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
|
||||
box-shadow: 0 4px 20px rgba(76, 175, 80, 0.3);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.etb-success-button:hover {
|
||||
background: linear-gradient(135deg, #2e7d32 0%, #4caf50 100%);
|
||||
box-shadow: 0 6px 25px rgba(76, 175, 80, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.etb-input-field {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.etb-input-field .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline {
|
||||
border-color: #1e3c72;
|
||||
}
|
||||
|
||||
.etb-input-field .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline {
|
||||
border-color: #1e3c72;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.etb-security-badge {
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%);
|
||||
border: 1px solid #e3f2fd;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.etb-security-badge:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.etb-chip-gradient {
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.etb-avatar-gradient {
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
box-shadow: 0 8px 32px rgba(30, 60, 114, 0.3);
|
||||
}
|
||||
|
||||
.etb-avatar-success {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
|
||||
}
|
||||
|
||||
.etb-fade-in {
|
||||
animation: fadeIn 1s ease-in-out;
|
||||
}
|
||||
|
||||
.etb-slide-up {
|
||||
animation: slideUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
.etb-pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(30, 60, 114, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(30, 60, 114, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(30, 60, 114, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.etb-glass-card {
|
||||
max-width: 420px;
|
||||
margin: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.etb-glass-card {
|
||||
max-width: 380px;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
.etb-logo-gradient {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.etb-gradient-bg {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.etb-glass-card {
|
||||
max-width: calc(100vw - 32px);
|
||||
margin: 16px;
|
||||
border-radius: 12px;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.etb-primary-button,
|
||||
.etb-success-button {
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
.etb-gradient-bg {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.etb-glass-card {
|
||||
max-width: calc(100vw - 24px);
|
||||
margin: 12px;
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
||||
.etb-gradient-bg {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Landscape orientation for mobile */
|
||||
@media (max-height: 600px) and (orientation: landscape) {
|
||||
.etb-glass-card {
|
||||
max-height: 90vh;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.etb-gradient-bg {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.etb-glass-card {
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.etb-security-badge {
|
||||
background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%);
|
||||
border: 1px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility improvements */
|
||||
.etb-primary-button:focus,
|
||||
.etb-success-button:focus {
|
||||
outline: 2px solid #1e3c72;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.etb-input-field .MuiOutlinedInput-root:focus-within {
|
||||
outline: 2px solid #1e3c72;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* High contrast mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.etb-logo-gradient {
|
||||
background: #000000;
|
||||
-webkit-text-fill-color: #000000;
|
||||
}
|
||||
|
||||
.etb-primary-button {
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.etb-success-button {
|
||||
background: #006600;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
666
etb-dashboard/src/components/Login/LoginPage.tsx
Normal file
666
etb-dashboard/src/components/Login/LoginPage.tsx
Normal file
@@ -0,0 +1,666 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { LoginFormData } from '../../types';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ETBLogo from './ETBLogo';
|
||||
// Use Material-UI icons for compatibility
|
||||
import {
|
||||
Visibility,
|
||||
VisibilityOff,
|
||||
Person,
|
||||
Lock,
|
||||
Security,
|
||||
VpnKey,
|
||||
Analytics,
|
||||
Timeline,
|
||||
Warning,
|
||||
CheckCircle,
|
||||
Smartphone,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
const { login, verifyMFA, setupMFA, isLoading, error, mfaRequired, clearError } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Form states
|
||||
const [formData, setFormData] = useState<LoginFormData>({
|
||||
username: '',
|
||||
password: '',
|
||||
mfa_token: '',
|
||||
remember_me: false,
|
||||
});
|
||||
|
||||
// UI states
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showMFA, setShowMFA] = useState(false);
|
||||
const [mfaStep, setMfaStep] = useState(0);
|
||||
const [mfaSetupData, setMfaSetupData] = useState<any>(null);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
// Security features
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [hasLoggedDeviceInfo, setHasLoggedDeviceInfo] = useState(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [hasLoggedLocationInfo, setHasLoggedLocationInfo] = useState(false);
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
const securityFeatures = [
|
||||
{
|
||||
icon: Security,
|
||||
title: 'Zero Trust Architecture',
|
||||
description: 'Never trust, always verify - Enterprise-grade security',
|
||||
status: 'active',
|
||||
compliance: ['SOX', 'HIPAA', 'GDPR']
|
||||
},
|
||||
{
|
||||
icon: VpnKey,
|
||||
title: 'Adaptive Authentication',
|
||||
description: 'Risk-based multi-factor authentication',
|
||||
status: 'active',
|
||||
compliance: ['PCI-DSS', 'ISO27001']
|
||||
},
|
||||
{
|
||||
icon: Analytics,
|
||||
title: 'AI Threat Detection',
|
||||
description: 'Machine learning-powered security analysis',
|
||||
status: 'active',
|
||||
compliance: ['SOX', 'HIPAA']
|
||||
},
|
||||
{
|
||||
icon: Timeline,
|
||||
title: 'Immutable Audit Trail',
|
||||
description: 'Tamper-proof compliance logging',
|
||||
status: 'active',
|
||||
compliance: ['SOX', 'HIPAA', 'GDPR', 'PCI-DSS']
|
||||
},
|
||||
];
|
||||
|
||||
// Memoize the device info gathering function
|
||||
const gatherDeviceInfo = useCallback(async () => {
|
||||
try {
|
||||
const deviceInfo = {
|
||||
userAgent: navigator.userAgent,
|
||||
platform: navigator.platform,
|
||||
language: navigator.language,
|
||||
screenResolution: `${window.screen.width}x${window.screen.height}`,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
cookieEnabled: navigator.cookieEnabled,
|
||||
onLine: navigator.onLine,
|
||||
hardwareConcurrency: navigator.hardwareConcurrency,
|
||||
deviceMemory: (navigator as any).deviceMemory,
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Device Info:', deviceInfo);
|
||||
}
|
||||
|
||||
sessionStorage.setItem('etb_device_info_logged', 'true');
|
||||
setHasLoggedDeviceInfo(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to gather device info:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Memoize the location info gathering function
|
||||
const gatherLocationInfo = useCallback(async () => {
|
||||
try {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const locationInfo = {
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
accuracy: position.coords.accuracy,
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Location Info:', locationInfo);
|
||||
}
|
||||
|
||||
sessionStorage.setItem('etb_location_info_logged', 'true');
|
||||
setHasLoggedLocationInfo(true);
|
||||
},
|
||||
(error) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Location access denied or failed:', error);
|
||||
}
|
||||
sessionStorage.setItem('etb_location_info_logged', 'true');
|
||||
setHasLoggedLocationInfo(true);
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to gather location info:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInitialized.current) return;
|
||||
|
||||
clearError();
|
||||
|
||||
const deviceInfoLogged = sessionStorage.getItem('etb_device_info_logged');
|
||||
const locationInfoLogged = sessionStorage.getItem('etb_location_info_logged');
|
||||
|
||||
if (deviceInfoLogged) {
|
||||
setHasLoggedDeviceInfo(true);
|
||||
}
|
||||
if (locationInfoLogged) {
|
||||
setHasLoggedLocationInfo(true);
|
||||
}
|
||||
|
||||
if (!deviceInfoLogged) {
|
||||
gatherDeviceInfo();
|
||||
}
|
||||
if (!locationInfoLogged) {
|
||||
gatherLocationInfo();
|
||||
}
|
||||
|
||||
setIsAnimating(true);
|
||||
hasInitialized.current = true;
|
||||
}, [clearError, gatherDeviceInfo, gatherLocationInfo]);
|
||||
|
||||
const handleInputChange = (field: keyof LoginFormData) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: event.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (field: keyof LoginFormData) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: event.target.checked,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleLogin = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
await login({
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
mfa_token: formData.mfa_token,
|
||||
});
|
||||
|
||||
if (mfaRequired) {
|
||||
setShowMFA(true);
|
||||
setMfaStep(1);
|
||||
} else {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMFAVerification = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
await verifyMFA(formData.mfa_token || '', '');
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('MFA verification failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMFASetup = async () => {
|
||||
try {
|
||||
const deviceName = `Device ${new Date().toLocaleDateString()}`;
|
||||
const setupData = await setupMFA(deviceName);
|
||||
setMfaSetupData(setupData);
|
||||
setMfaStep(2);
|
||||
} catch (error) {
|
||||
console.error('MFA setup failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMFASetupComplete = async () => {
|
||||
try {
|
||||
await verifyMFA('', mfaSetupData?.device?.id);
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('MFA setup completion failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderETBLogo = () => (
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="transform scale-75 sm:scale-90 transition-transform duration-300">
|
||||
<ETBLogo size="small" showSubtitle={true} variant="horizontal" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderSecurityBadges = () => (
|
||||
<div className="w-full">
|
||||
<div className="text-center mb-3">
|
||||
<h3 className="text-sm font-bold text-white mb-1">
|
||||
Enterprise Security Platform
|
||||
</h3>
|
||||
<div className="flex justify-center items-center space-x-2 text-xs text-gray-300">
|
||||
<div className="flex items-center">
|
||||
<div className="w-1.5 h-1.5 bg-green-400 rounded-full mr-1"></div>
|
||||
<span>All Systems Operational</span>
|
||||
</div>
|
||||
<span>•</span>
|
||||
<span>Zero Trust Enabled</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{securityFeatures.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`etb-security-badge animate-fade-in relative overflow-hidden`}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(30, 41, 59, 0.8) 100%)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
borderRadius: '12px',
|
||||
padding: '16px 12px',
|
||||
textAlign: 'left',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.3)',
|
||||
animationDelay: `${index * 100}ms`,
|
||||
position: 'relative',
|
||||
backdropFilter: 'blur(10px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px'
|
||||
}}
|
||||
>
|
||||
{/* Status indicator */}
|
||||
<div className="absolute top-2 right-2">
|
||||
<div className={`w-2 h-2 rounded-full ${feature.status === 'active' ? 'bg-green-400' : 'bg-red-400'}`}></div>
|
||||
</div>
|
||||
|
||||
{/* Compliance badges */}
|
||||
<div className="absolute top-2 left-2 flex space-x-1">
|
||||
{feature.compliance.slice(0, 2).map((comp, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="text-[9px] px-1.5 py-0.5 bg-blue-500/20 text-blue-300 rounded font-medium border border-blue-400/30"
|
||||
>
|
||||
{comp}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Icon */}
|
||||
<div className="flex-shrink-0 mt-4">
|
||||
<feature.icon className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0 mt-4">
|
||||
<h4 className="text-sm font-bold text-white mb-1 leading-tight">
|
||||
{feature.title}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-300 leading-tight">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Security status footer */}
|
||||
<div className="mt-2 p-2 bg-gradient-to-r from-green-500/20 to-blue-500/20 border border-green-400/30 rounded-lg backdrop-blur-sm">
|
||||
<div className="flex items-center justify-center space-x-3 text-[10px]">
|
||||
<div className="flex items-center text-green-300">
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
<span className="font-medium">SECURE</span>
|
||||
</div>
|
||||
<div className="flex items-center text-blue-300">
|
||||
<Security className="w-3 h-3 mr-1" />
|
||||
<span className="font-medium">LOW RISK</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderLoginForm = () => (
|
||||
<div className="flex flex-col lg:flex-row gap-6 h-full">
|
||||
{/* Left side - Login Form */}
|
||||
<div className="flex-1 flex flex-col justify-center">
|
||||
<form onSubmit={handleLogin} className="w-full space-y-4">
|
||||
{renderETBLogo()}
|
||||
|
||||
<div className="text-center">
|
||||
<div className="mb-2">
|
||||
<span className="inline-block px-3 py-1 bg-blue-500/20 text-blue-300 text-xs font-semibold rounded-full border border-blue-400/30 backdrop-blur-sm">
|
||||
ENTERPRISE SECURITY
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-white mb-1">
|
||||
Secure Access Portal
|
||||
</h1>
|
||||
<p className="text-xs sm:text-sm text-gray-300 leading-relaxed mb-2">
|
||||
Enterprise-grade incident management and security platform
|
||||
</p>
|
||||
<div className="flex justify-center items-center space-x-3 text-xs text-gray-400">
|
||||
<div className="flex items-center">
|
||||
<div className="w-1.5 h-1.5 bg-green-400 rounded-full mr-1"></div>
|
||||
<span>Zero Trust</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="w-1.5 h-1.5 bg-blue-400 rounded-full mr-1"></div>
|
||||
<span>Compliance Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/20 border border-red-400/30 rounded-lg p-3 animate-slide-up backdrop-blur-sm">
|
||||
<div className="flex items-center">
|
||||
<Warning className="w-4 h-4 text-red-400 mr-2" />
|
||||
<p className="text-xs font-medium text-red-300">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Person className="h-4 w-4 text-blue-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={formData.username}
|
||||
onChange={handleInputChange('username')}
|
||||
required
|
||||
autoComplete="username"
|
||||
className="etb-input-field pl-9 h-10 text-sm"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px 8px 36px',
|
||||
border: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
borderRadius: '10px',
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s ease',
|
||||
background: 'rgba(15, 23, 42, 0.8)',
|
||||
color: 'white',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock className="h-4 w-4 text-blue-400" />
|
||||
</div>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Password"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange('password')}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
className="etb-input-field pl-9 pr-9 h-10 text-sm"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 36px 8px 36px',
|
||||
border: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
borderRadius: '10px',
|
||||
fontSize: '14px',
|
||||
height: '40px',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s ease',
|
||||
background: 'rgba(15, 23, 42, 0.8)',
|
||||
color: 'white',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
>
|
||||
{showPassword ? (
|
||||
<VisibilityOff className="h-4 w-4 text-blue-400" />
|
||||
) : (
|
||||
<Visibility className="h-4 w-4 text-blue-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="remember_me"
|
||||
checked={formData.remember_me}
|
||||
onChange={handleCheckboxChange('remember_me')}
|
||||
className="h-3 w-3 text-blue-400 focus:ring-blue-400 border-gray-500 rounded bg-slate-800"
|
||||
/>
|
||||
<label htmlFor="remember_me" className="ml-2 text-xs text-gray-300">
|
||||
Remember me for 30 days
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="etb-primary-button w-full h-10 text-sm font-bold disabled:opacity-50 disabled:cursor-not-allowed relative overflow-hidden"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #667eea 100%)',
|
||||
color: 'white',
|
||||
fontWeight: '700',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '12px',
|
||||
border: '2px solid rgba(255, 255, 255, 0.2)',
|
||||
width: '100%',
|
||||
height: '40px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 4px 16px rgba(30, 60, 114, 0.4)',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{/* Enterprise gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent opacity-0 hover:opacity-10 transition-opacity duration-300"></div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center relative z-10">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
<span className="font-bold">Authenticating...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center relative z-10">
|
||||
<Security className="w-4 h-4 mr-2" />
|
||||
<span className="font-bold">Secure Sign In</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Enterprise security indicator */}
|
||||
<div className="absolute top-1 right-1">
|
||||
<div className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Right side - Security Features */}
|
||||
<div className="flex-1 flex flex-col justify-center">
|
||||
{renderSecurityBadges()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderMFAForm = () => (
|
||||
<div className="w-full space-y-6">
|
||||
{renderETBLogo()}
|
||||
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-etb-blue-500 mb-2">
|
||||
Multi-Factor Authentication
|
||||
</h2>
|
||||
<p className="text-sm sm:text-base text-gray-600">
|
||||
Enter your authentication code to continue
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleMFAVerification} className="space-y-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<VpnKey className="h-5 w-5 text-etb-blue-500" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter 6-digit code"
|
||||
value={formData.mfa_token}
|
||||
onChange={handleInputChange('mfa_token')}
|
||||
required
|
||||
className="etb-input-field pl-10 h-12 sm:h-14 text-center text-lg tracking-widest"
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="etb-primary-button w-full h-12 sm:h-14"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||
Verifying...
|
||||
</div>
|
||||
) : (
|
||||
'Verify & Continue'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderMFASetup = () => (
|
||||
<div className="w-full space-y-6">
|
||||
{renderETBLogo()}
|
||||
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-etb-blue-500 mb-2">
|
||||
Setup Multi-Factor Authentication
|
||||
</h2>
|
||||
<p className="text-sm sm:text-base text-gray-600">
|
||||
Secure your account with two-factor authentication
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{mfaStep === 1 && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6 text-center">
|
||||
<Smartphone className="w-12 h-12 text-etb-blue-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-etb-blue-500 mb-2">
|
||||
Ready to Setup MFA?
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
We'll help you set up two-factor authentication for enhanced security.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleMFASetup}
|
||||
disabled={isLoading}
|
||||
className="etb-primary-button"
|
||||
>
|
||||
{isLoading ? 'Setting up...' : 'Start Setup'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mfaStep === 2 && mfaSetupData && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-6 text-center">
|
||||
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-green-700 mb-2">
|
||||
Scan QR Code
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Use your authenticator app to scan this QR code
|
||||
</p>
|
||||
<div className="flex justify-center mb-4">
|
||||
<QRCodeSVG value={mfaSetupData.qr_code_url} size={200} />
|
||||
</div>
|
||||
<button
|
||||
onClick={handleMFASetupComplete}
|
||||
disabled={isLoading}
|
||||
className="etb-primary-button"
|
||||
>
|
||||
{isLoading ? 'Completing...' : 'Complete Setup'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 h-screen w-screen overflow-hidden bg-gradient-to-br from-etb-blue-500 via-etb-blue-600 to-blue-700 flex items-center justify-center p-4 sm:p-6 lg:p-8"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #667eea 100%)',
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '1rem'
|
||||
}}
|
||||
>
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_20%_80%,rgba(120,119,198,0.3)_0%,transparent_50%)]"></div>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_80%_20%,rgba(255,255,255,0.1)_0%,transparent_50%)]"></div>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_40%_40%,rgba(120,119,198,0.2)_0%,transparent_50%)]"></div>
|
||||
</div>
|
||||
|
||||
{/* Main Card */}
|
||||
<div
|
||||
className={`etb-glass-card w-full max-w-4xl h-[60vh] animate-fade-in relative ${
|
||||
isAnimating ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
|
||||
} transition-all duration-1000 ease-out`}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.95) 50%, rgba(51, 65, 85, 0.95) 100%)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '2px solid rgba(59, 130, 246, 0.3)',
|
||||
borderRadius: '24px',
|
||||
boxShadow: '0 32px 64px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(59, 130, 246, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
|
||||
width: '100%',
|
||||
height: '60vh',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
{/* Enterprise security border glow */}
|
||||
<div className="absolute inset-0 rounded-3xl bg-gradient-to-r from-blue-500/20 via-cyan-500/10 to-blue-500/20 opacity-60 pointer-events-none"></div>
|
||||
|
||||
{/* Security status indicator */}
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<div className="flex items-center space-x-2 bg-green-500/20 px-3 py-1.5 rounded-full border border-green-400/30 backdrop-blur-sm">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<span className="text-xs font-medium text-green-300">SECURE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-6 sm:p-8 lg:p-10 overflow-hidden">
|
||||
{!showMFA && renderLoginForm()}
|
||||
{showMFA && mfaStep === 0 && renderMFAForm()}
|
||||
{showMFA && mfaStep > 0 && renderMFASetup()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
475
etb-dashboard/src/components/Login/LoginPageTailwind.tsx
Normal file
475
etb-dashboard/src/components/Login/LoginPageTailwind.tsx
Normal file
@@ -0,0 +1,475 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { LoginFormData } from '../../types';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ETBLogo from './ETBLogo';
|
||||
import {
|
||||
EyeIcon,
|
||||
EyeSlashIcon,
|
||||
UserIcon,
|
||||
LockClosedIcon,
|
||||
ShieldCheckIcon,
|
||||
KeyIcon,
|
||||
ChartBarIcon,
|
||||
ClockIcon,
|
||||
CloudIcon,
|
||||
CheckBadgeIcon,
|
||||
ExclamationTriangleIcon,
|
||||
CheckCircleIcon,
|
||||
DevicePhoneMobileIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
const { login, verifyMFA, setupMFA, isLoading, error, mfaRequired, clearError } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Form states
|
||||
const [formData, setFormData] = useState<LoginFormData>({
|
||||
username: '',
|
||||
password: '',
|
||||
mfa_token: '',
|
||||
remember_me: false,
|
||||
});
|
||||
|
||||
// UI states
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showMFA, setShowMFA] = useState(false);
|
||||
const [mfaStep, setMfaStep] = useState(0);
|
||||
const [mfaSetupData, setMfaSetupData] = useState<any>(null);
|
||||
const [mfaDevices] = useState<any[]>([]);
|
||||
const [selectedDevice, setSelectedDevice] = useState<string>('');
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
// Security features
|
||||
const [riskAssessment] = useState<any>(null);
|
||||
const [hasLoggedDeviceInfo, setHasLoggedDeviceInfo] = useState(false);
|
||||
const [hasLoggedLocationInfo, setHasLoggedLocationInfo] = useState(false);
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
const securityFeatures = [
|
||||
{ icon: ShieldCheckIcon, title: 'Zero Trust', description: 'Never trust, always verify' },
|
||||
{ icon: KeyIcon, title: 'Multi-Factor Auth', description: 'Enhanced security layers' },
|
||||
{ icon: ChartBarIcon, title: 'Risk Assessment', description: 'AI-powered threat analysis' },
|
||||
{ icon: ClockIcon, title: 'Audit Logging', description: 'Complete activity tracking' },
|
||||
];
|
||||
|
||||
// Memoize the device info gathering function
|
||||
const gatherDeviceInfo = useCallback(async () => {
|
||||
try {
|
||||
const deviceInfo = {
|
||||
userAgent: navigator.userAgent,
|
||||
platform: navigator.platform,
|
||||
language: navigator.language,
|
||||
screenResolution: `${window.screen.width}x${window.screen.height}`,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
cookieEnabled: navigator.cookieEnabled,
|
||||
onLine: navigator.onLine,
|
||||
hardwareConcurrency: navigator.hardwareConcurrency,
|
||||
deviceMemory: (navigator as any).deviceMemory,
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Device Info:', deviceInfo);
|
||||
}
|
||||
|
||||
sessionStorage.setItem('etb_device_info_logged', 'true');
|
||||
setHasLoggedDeviceInfo(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to gather device info:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Memoize the location info gathering function
|
||||
const gatherLocationInfo = useCallback(async () => {
|
||||
try {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const locationInfo = {
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
accuracy: position.coords.accuracy,
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Location Info:', locationInfo);
|
||||
}
|
||||
|
||||
sessionStorage.setItem('etb_location_info_logged', 'true');
|
||||
setHasLoggedLocationInfo(true);
|
||||
},
|
||||
(error) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Location access denied or failed:', error);
|
||||
}
|
||||
sessionStorage.setItem('etb_location_info_logged', 'true');
|
||||
setHasLoggedLocationInfo(true);
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to gather location info:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInitialized.current) return;
|
||||
|
||||
clearError();
|
||||
|
||||
const deviceInfoLogged = sessionStorage.getItem('etb_device_info_logged');
|
||||
const locationInfoLogged = sessionStorage.getItem('etb_location_info_logged');
|
||||
|
||||
if (deviceInfoLogged) {
|
||||
setHasLoggedDeviceInfo(true);
|
||||
}
|
||||
if (locationInfoLogged) {
|
||||
setHasLoggedLocationInfo(true);
|
||||
}
|
||||
|
||||
if (!deviceInfoLogged) {
|
||||
gatherDeviceInfo();
|
||||
}
|
||||
if (!locationInfoLogged) {
|
||||
gatherLocationInfo();
|
||||
}
|
||||
|
||||
setIsAnimating(true);
|
||||
hasInitialized.current = true;
|
||||
}, [clearError, gatherDeviceInfo, gatherLocationInfo]);
|
||||
|
||||
const handleInputChange = (field: keyof LoginFormData) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: event.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (field: keyof LoginFormData) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: event.target.checked,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleLogin = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
await login({
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
mfa_token: formData.mfa_token,
|
||||
});
|
||||
|
||||
if (mfaRequired) {
|
||||
setShowMFA(true);
|
||||
setMfaStep(1);
|
||||
} else {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMFAVerification = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
await verifyMFA(formData.mfa_token || '', selectedDevice);
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('MFA verification failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMFASetup = async () => {
|
||||
try {
|
||||
const deviceName = `Device ${new Date().toLocaleDateString()}`;
|
||||
const setupData = await setupMFA(deviceName);
|
||||
setMfaSetupData(setupData);
|
||||
setMfaStep(2);
|
||||
} catch (error) {
|
||||
console.error('MFA setup failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMFASetupComplete = async () => {
|
||||
try {
|
||||
await verifyMFA('', mfaSetupData?.device?.id);
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('MFA setup completion failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderETBLogo = () => (
|
||||
<div className="flex justify-center mb-6 sm:mb-8">
|
||||
<div className="transform scale-90 sm:scale-100 transition-transform duration-300">
|
||||
<ETBLogo size="medium" showSubtitle={true} variant="horizontal" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderSecurityBadges = () => (
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h3 className="text-lg sm:text-xl font-semibold text-center mb-4 sm:mb-6 text-etb-blue-500">
|
||||
Enterprise Security Features
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-4">
|
||||
{securityFeatures.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`etb-security-badge animate-fade-in`}
|
||||
style={{ animationDelay: `${index * 200}ms` }}
|
||||
>
|
||||
<feature.icon className="w-6 h-6 sm:w-7 sm:h-7 text-etb-blue-500 mx-auto mb-2" />
|
||||
<h4 className="text-xs sm:text-sm font-semibold text-gray-800 mb-1 leading-tight">
|
||||
{feature.title}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-600 leading-tight">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderLoginForm = () => (
|
||||
<form onSubmit={handleLogin} className="w-full space-y-4 sm:space-y-6">
|
||||
{renderETBLogo()}
|
||||
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-etb-blue-500 mb-2">
|
||||
Secure Access Portal
|
||||
</h1>
|
||||
<p className="text-sm sm:text-base text-gray-600 leading-relaxed">
|
||||
Enterprise-grade incident management and security platform
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4 animate-slide-up">
|
||||
<div className="flex items-center">
|
||||
<ExclamationTriangleIcon className="w-5 h-5 text-red-500 mr-3" />
|
||||
<p className="text-sm font-medium text-red-800">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<UserIcon className="h-5 w-5 text-etb-blue-500" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={formData.username}
|
||||
onChange={handleInputChange('username')}
|
||||
required
|
||||
autoComplete="username"
|
||||
className="etb-input-field pl-10 h-12 sm:h-14 text-sm sm:text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<LockClosedIcon className="h-5 w-5 text-etb-blue-500" />
|
||||
</div>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Password"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange('password')}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
className="etb-input-field pl-10 pr-10 h-12 sm:h-14 text-sm sm:text-base"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeSlashIcon className="h-5 w-5 text-etb-blue-500" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5 text-etb-blue-500" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="remember_me"
|
||||
checked={formData.remember_me}
|
||||
onChange={handleCheckboxChange('remember_me')}
|
||||
className="h-4 w-4 text-etb-blue-500 focus:ring-etb-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="remember_me" className="ml-2 text-sm text-gray-700">
|
||||
Remember me for 30 days
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="etb-primary-button w-full h-12 sm:h-14 text-sm sm:text-base font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||
Signing In...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center">
|
||||
<ShieldCheckIcon className="w-5 h-5 mr-2" />
|
||||
Sign In
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{renderSecurityBadges()}
|
||||
</form>
|
||||
);
|
||||
|
||||
const renderMFAForm = () => (
|
||||
<div className="w-full space-y-6">
|
||||
{renderETBLogo()}
|
||||
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-etb-blue-500 mb-2">
|
||||
Multi-Factor Authentication
|
||||
</h2>
|
||||
<p className="text-sm sm:text-base text-gray-600">
|
||||
Enter your authentication code to continue
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleMFAVerification} className="space-y-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<KeyIcon className="h-5 w-5 text-etb-blue-500" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter 6-digit code"
|
||||
value={formData.mfa_token}
|
||||
onChange={handleInputChange('mfa_token')}
|
||||
required
|
||||
className="etb-input-field pl-10 h-12 sm:h-14 text-center text-lg tracking-widest"
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="etb-primary-button w-full h-12 sm:h-14"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||
Verifying...
|
||||
</div>
|
||||
) : (
|
||||
'Verify & Continue'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderMFASetup = () => (
|
||||
<div className="w-full space-y-6">
|
||||
{renderETBLogo()}
|
||||
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-etb-blue-500 mb-2">
|
||||
Setup Multi-Factor Authentication
|
||||
</h2>
|
||||
<p className="text-sm sm:text-base text-gray-600">
|
||||
Secure your account with two-factor authentication
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{mfaStep === 1 && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6 text-center">
|
||||
<DevicePhoneMobileIcon className="w-12 h-12 text-etb-blue-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-etb-blue-500 mb-2">
|
||||
Ready to Setup MFA?
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
We'll help you set up two-factor authentication for enhanced security.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleMFASetup}
|
||||
disabled={isLoading}
|
||||
className="etb-primary-button"
|
||||
>
|
||||
{isLoading ? 'Setting up...' : 'Start Setup'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mfaStep === 2 && mfaSetupData && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-6 text-center">
|
||||
<CheckCircleIcon className="w-12 h-12 text-green-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-green-700 mb-2">
|
||||
Scan QR Code
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Use your authenticator app to scan this QR code
|
||||
</p>
|
||||
<div className="flex justify-center mb-4">
|
||||
<QRCodeSVG value={mfaSetupData.qr_code_url} size={200} />
|
||||
</div>
|
||||
<button
|
||||
onClick={handleMFASetupComplete}
|
||||
disabled={isLoading}
|
||||
className="etb-primary-button"
|
||||
>
|
||||
{isLoading ? 'Completing...' : 'Complete Setup'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 h-screen w-screen overflow-hidden bg-gradient-to-br from-etb-blue-500 via-etb-blue-600 to-blue-700 flex items-center justify-center p-4 sm:p-6 lg:p-8">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_20%_80%,rgba(120,119,198,0.3)_0%,transparent_50%)]"></div>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_80%_20%,rgba(255,255,255,0.1)_0%,transparent_50%)]"></div>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_40%_40%,rgba(120,119,198,0.2)_0%,transparent_50%)]"></div>
|
||||
</div>
|
||||
|
||||
{/* Main Card */}
|
||||
<div className={`etb-glass-card w-full max-w-md sm:max-w-lg lg:max-w-xl max-h-[90vh] sm:max-h-[85vh] lg:max-h-[80vh] overflow-y-auto animate-fade-in ${
|
||||
isAnimating ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
|
||||
} transition-all duration-1000 ease-out`}>
|
||||
<div className="p-6 sm:p-8 lg:p-10">
|
||||
{!showMFA && renderLoginForm()}
|
||||
{showMFA && mfaStep === 0 && renderMFAForm()}
|
||||
{showMFA && mfaStep > 0 && renderMFASetup()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
250
etb-dashboard/src/contexts/AuthContext.tsx
Normal file
250
etb-dashboard/src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import React, { createContext, useContext, useReducer, useEffect, ReactNode } from 'react';
|
||||
import { User, LoginRequest, LoginResponse, ApiError } from '../types';
|
||||
import apiService from '../services/api';
|
||||
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
mfaRequired: boolean;
|
||||
mfaDevices: any[];
|
||||
}
|
||||
|
||||
interface AuthContextType extends AuthState {
|
||||
login: (credentials: LoginRequest) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
verifyMFA: (token: string, deviceId?: string) => Promise<void>;
|
||||
setupMFA: (deviceName: string) => Promise<any>;
|
||||
clearError: () => void;
|
||||
refreshUser: () => Promise<void>;
|
||||
}
|
||||
|
||||
type AuthAction =
|
||||
| { type: 'LOGIN_START' }
|
||||
| { type: 'LOGIN_SUCCESS'; payload: { user: User; mfaRequired: boolean } }
|
||||
| { type: 'LOGIN_FAILURE'; payload: string }
|
||||
| { type: 'LOGOUT' }
|
||||
| { type: 'MFA_VERIFIED'; payload: User }
|
||||
| { type: 'MFA_SETUP'; payload: any[] }
|
||||
| { type: 'CLEAR_ERROR' }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_USER'; payload: User };
|
||||
|
||||
const initialState: AuthState = {
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
mfaRequired: false,
|
||||
mfaDevices: [],
|
||||
};
|
||||
|
||||
const authReducer = (state: AuthState, action: AuthAction): AuthState => {
|
||||
switch (action.type) {
|
||||
case 'LOGIN_START':
|
||||
return {
|
||||
...state,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
};
|
||||
case 'LOGIN_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
isAuthenticated: !action.payload.mfaRequired,
|
||||
user: action.payload.mfaRequired ? null : action.payload.user,
|
||||
mfaRequired: action.payload.mfaRequired,
|
||||
error: null,
|
||||
};
|
||||
case 'LOGIN_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
mfaRequired: false,
|
||||
error: action.payload,
|
||||
};
|
||||
case 'LOGOUT':
|
||||
return {
|
||||
...initialState,
|
||||
};
|
||||
case 'MFA_VERIFIED':
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
isAuthenticated: true,
|
||||
user: action.payload,
|
||||
mfaRequired: false,
|
||||
error: null,
|
||||
};
|
||||
case 'MFA_SETUP':
|
||||
return {
|
||||
...state,
|
||||
mfaDevices: action.payload,
|
||||
};
|
||||
case 'CLEAR_ERROR':
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
};
|
||||
case 'SET_LOADING':
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload,
|
||||
};
|
||||
case 'SET_USER':
|
||||
return {
|
||||
...state,
|
||||
user: action.payload,
|
||||
isAuthenticated: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(authReducer, initialState);
|
||||
|
||||
// Check for existing authentication on mount
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const token = apiService.getToken();
|
||||
if (token) {
|
||||
try {
|
||||
dispatch({ type: 'SET_LOADING', payload: true });
|
||||
const user = await apiService.getCurrentUser();
|
||||
dispatch({ type: 'SET_USER', payload: user });
|
||||
} catch (error) {
|
||||
console.error('Failed to verify existing authentication:', error);
|
||||
apiService.clearToken();
|
||||
} finally {
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
const login = async (credentials: LoginRequest): Promise<void> => {
|
||||
try {
|
||||
dispatch({ type: 'LOGIN_START' });
|
||||
|
||||
const response: LoginResponse = await apiService.login(credentials);
|
||||
|
||||
if (response.user.mfa_enabled && !credentials.mfa_token) {
|
||||
// MFA is enabled but no token provided
|
||||
dispatch({
|
||||
type: 'LOGIN_SUCCESS',
|
||||
payload: { user: response.user, mfaRequired: true }
|
||||
});
|
||||
} else {
|
||||
// Login successful
|
||||
dispatch({
|
||||
type: 'LOGIN_SUCCESS',
|
||||
payload: { user: response.user, mfaRequired: false }
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const apiError = error as ApiError;
|
||||
dispatch({
|
||||
type: 'LOGIN_FAILURE',
|
||||
payload: apiError.message || 'Login failed'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async (): Promise<void> => {
|
||||
try {
|
||||
await apiService.logout();
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
} finally {
|
||||
dispatch({ type: 'LOGOUT' });
|
||||
}
|
||||
};
|
||||
|
||||
const verifyMFA = async (token: string, deviceId?: string): Promise<void> => {
|
||||
try {
|
||||
dispatch({ type: 'SET_LOADING', payload: true });
|
||||
|
||||
const verified = await apiService.verifyMFA(token, deviceId);
|
||||
|
||||
if (verified) {
|
||||
const user = await apiService.getCurrentUser();
|
||||
dispatch({ type: 'MFA_VERIFIED', payload: user });
|
||||
} else {
|
||||
throw new Error('Invalid MFA token');
|
||||
}
|
||||
} catch (error) {
|
||||
const apiError = error as ApiError;
|
||||
dispatch({
|
||||
type: 'LOGIN_FAILURE',
|
||||
payload: apiError.message || 'MFA verification failed'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const setupMFA = async (deviceName: string): Promise<any> => {
|
||||
try {
|
||||
const setupData = await apiService.setupMFA(deviceName);
|
||||
return setupData;
|
||||
} catch (error) {
|
||||
const apiError = error as ApiError;
|
||||
dispatch({
|
||||
type: 'LOGIN_FAILURE',
|
||||
payload: apiError.message || 'MFA setup failed'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const clearError = (): void => {
|
||||
dispatch({ type: 'CLEAR_ERROR' });
|
||||
};
|
||||
|
||||
const refreshUser = async (): Promise<void> => {
|
||||
try {
|
||||
const user = await apiService.getCurrentUser();
|
||||
dispatch({ type: 'SET_USER', payload: user });
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh user:', error);
|
||||
dispatch({ type: 'LOGOUT' });
|
||||
}
|
||||
};
|
||||
|
||||
const contextValue: AuthContextType = {
|
||||
...state,
|
||||
login,
|
||||
logout,
|
||||
verifyMFA,
|
||||
setupMFA,
|
||||
clearError,
|
||||
refreshUser,
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
49
etb-dashboard/src/index.css
Normal file
49
etb-dashboard/src/index.css
Normal file
@@ -0,0 +1,49 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.etb-glass-card {
|
||||
@apply bg-white backdrop-blur-xl border border-white/30 rounded-2xl shadow-2xl;
|
||||
box-shadow:
|
||||
0 25px 50px rgba(0, 0, 0, 0.15),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.etb-primary-button {
|
||||
@apply bg-gradient-to-r from-etb-blue-500 to-etb-blue-600 text-white font-semibold py-3 px-6 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-0.5;
|
||||
}
|
||||
|
||||
.etb-input-field {
|
||||
@apply w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-etb-blue-500 focus:border-etb-blue-500 transition-colors duration-200;
|
||||
}
|
||||
|
||||
.etb-security-badge {
|
||||
@apply bg-gradient-to-br from-gray-50 to-white border border-gray-200 rounded-xl p-4 text-center transition-all duration-300 hover:-translate-y-1 hover:shadow-lg;
|
||||
}
|
||||
}
|
||||
14
etb-dashboard/src/index.tsx
Normal file
14
etb-dashboard/src/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
1
etb-dashboard/src/logo.svg
Normal file
1
etb-dashboard/src/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
1
etb-dashboard/src/react-app-env.d.ts
vendored
Normal file
1
etb-dashboard/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
15
etb-dashboard/src/reportWebVitals.ts
Normal file
15
etb-dashboard/src/reportWebVitals.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
434
etb-dashboard/src/services/api.ts
Normal file
434
etb-dashboard/src/services/api.ts
Normal file
@@ -0,0 +1,434 @@
|
||||
import axios from 'axios';
|
||||
import {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
User,
|
||||
Incident,
|
||||
SLADefinition,
|
||||
SLAInstance,
|
||||
OnCallRotation,
|
||||
OnCallAssignment,
|
||||
MonitoringTarget,
|
||||
HealthCheck,
|
||||
SystemMetric,
|
||||
MetricMeasurement,
|
||||
Alert,
|
||||
AuditLog,
|
||||
RiskAssessment,
|
||||
DevicePosture,
|
||||
ComplianceFramework,
|
||||
KnowledgeArticle,
|
||||
Postmortem,
|
||||
Runbook,
|
||||
WarRoom,
|
||||
ChatMessage,
|
||||
Dashboard,
|
||||
MFADevice,
|
||||
MFASetupResponse,
|
||||
ChangePasswordFormData,
|
||||
CreateIncidentFormData,
|
||||
FilterOptions,
|
||||
PaginatedResponse,
|
||||
ApiError
|
||||
} from '../types';
|
||||
|
||||
// API Configuration
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL || process.env.REACT_APP_API_BASE_URL || 'http://localhost:8000';
|
||||
|
||||
class ApiService {
|
||||
private api: any;
|
||||
private token: string | null = null;
|
||||
|
||||
constructor() {
|
||||
this.api = axios.create({
|
||||
baseURL: `${API_BASE_URL}/api/v1`,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor to add auth token
|
||||
this.api.interceptors.request.use(
|
||||
(config: any) => {
|
||||
if (this.token) {
|
||||
config.headers.Authorization = `Token ${this.token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error: any) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor for error handling
|
||||
this.api.interceptors.response.use(
|
||||
(response: any) => {
|
||||
return response;
|
||||
},
|
||||
(error: any) => {
|
||||
if (error.response?.status === 401) {
|
||||
this.clearToken();
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(this.handleError(error));
|
||||
}
|
||||
);
|
||||
|
||||
// Load token from localStorage on initialization
|
||||
this.loadToken();
|
||||
}
|
||||
|
||||
private loadToken(): void {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
this.setToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(error: any): ApiError {
|
||||
const apiError: ApiError = {
|
||||
message: 'An unexpected error occurred',
|
||||
status: error.response?.status,
|
||||
};
|
||||
|
||||
if (error.response?.data) {
|
||||
const data = error.response.data as any;
|
||||
apiError.message = data.message || data.detail || apiError.message;
|
||||
apiError.code = data.code;
|
||||
apiError.details = data.errors || data.details;
|
||||
} else if (error.request) {
|
||||
apiError.message = 'Network error - please check your connection';
|
||||
}
|
||||
|
||||
return apiError;
|
||||
}
|
||||
|
||||
public setToken(token: string): void {
|
||||
this.token = token;
|
||||
localStorage.setItem('auth_token', token);
|
||||
}
|
||||
|
||||
public clearToken(): void {
|
||||
this.token = null;
|
||||
localStorage.removeItem('auth_token');
|
||||
}
|
||||
|
||||
public getToken(): string | null {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
// Authentication endpoints
|
||||
async login(credentials: LoginRequest): Promise<LoginResponse> {
|
||||
const response = await this.api.post('/security/api/auth/login/', credentials);
|
||||
const data = response.data;
|
||||
|
||||
if (data.token) {
|
||||
this.setToken(data.token);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
await this.api.post('/security/api/auth/logout/');
|
||||
} finally {
|
||||
this.clearToken();
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentUser(): Promise<User> {
|
||||
const response = await this.api.get('/security/api/auth/profile/');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async changePassword(data: ChangePasswordFormData): Promise<void> {
|
||||
await this.api.post('/security/api/auth/change-password/', data);
|
||||
}
|
||||
|
||||
async getMFAStatus(): Promise<{ mfa_enabled: boolean; devices: MFADevice[] }> {
|
||||
const response = await this.api.get('/security/api/auth/mfa-status/');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async setupMFA(deviceName: string): Promise<MFASetupResponse> {
|
||||
const response = await this.api.post('/security/api/mfa-devices/', {
|
||||
name: deviceName,
|
||||
device_type: 'TOTP'
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async verifyMFA(token: string, deviceId?: string): Promise<boolean> {
|
||||
const response = await this.api.post('/security/api/mfa-devices/verify/', {
|
||||
token,
|
||||
device_id: deviceId
|
||||
});
|
||||
return response.data.verified;
|
||||
}
|
||||
|
||||
// Incident Management endpoints
|
||||
async getIncidents(filters?: FilterOptions): Promise<PaginatedResponse<Incident>> {
|
||||
const response = await this.api.get('/incidents/incidents/', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getIncident(id: string): Promise<Incident> {
|
||||
const response = await this.api.get(`/incidents/incidents/${id}/`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createIncident(data: CreateIncidentFormData): Promise<Incident> {
|
||||
const response = await this.api.post('/incidents/incidents/', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateIncident(id: string, data: Partial<CreateIncidentFormData>): Promise<Incident> {
|
||||
const response = await this.api.patch(`/incidents/incidents/${id}/`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteIncident(id: string): Promise<void> {
|
||||
await this.api.delete(`/incidents/incidents/${id}/`);
|
||||
}
|
||||
|
||||
// SLA Management endpoints
|
||||
async getSLADefinitions(): Promise<SLADefinition[]> {
|
||||
const response = await this.api.get('/sla/sla-definitions/');
|
||||
return response.data.results || response.data;
|
||||
}
|
||||
|
||||
async getSLAInstances(filters?: FilterOptions): Promise<PaginatedResponse<SLAInstance>> {
|
||||
const response = await this.api.get('/sla/sla-instances/', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// On-Call Management endpoints
|
||||
async getOnCallRotations(): Promise<OnCallRotation[]> {
|
||||
const response = await this.api.get('/sla/on-call-rotations/');
|
||||
return response.data.results || response.data;
|
||||
}
|
||||
|
||||
async getOnCallAssignments(filters?: FilterOptions): Promise<PaginatedResponse<OnCallAssignment>> {
|
||||
const response = await this.api.get('/sla/on-call-assignments/', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getCurrentOnCall(): Promise<OnCallAssignment | null> {
|
||||
const response = await this.api.get('/sla/on-call-assignments/current/');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Monitoring endpoints
|
||||
async getMonitoringTargets(): Promise<MonitoringTarget[]> {
|
||||
const response = await this.api.get('/monitoring/monitoring-targets/');
|
||||
return response.data.results || response.data;
|
||||
}
|
||||
|
||||
async getHealthChecks(filters?: FilterOptions): Promise<PaginatedResponse<HealthCheck>> {
|
||||
const response = await this.api.get('/monitoring/health-checks/', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getSystemMetrics(): Promise<SystemMetric[]> {
|
||||
const response = await this.api.get('/monitoring/system-metrics/');
|
||||
return response.data.results || response.data;
|
||||
}
|
||||
|
||||
async getMetricMeasurements(metricId: string, filters?: FilterOptions): Promise<PaginatedResponse<MetricMeasurement>> {
|
||||
const response = await this.api.get(`/monitoring/metric-measurements/?metric=${metricId}`, { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getAlerts(filters?: FilterOptions): Promise<PaginatedResponse<Alert>> {
|
||||
const response = await this.api.get('/monitoring/alerts/', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async acknowledgeAlert(alertId: string): Promise<Alert> {
|
||||
const response = await this.api.patch(`/monitoring/alerts/${alertId}/`, { status: 'ACKNOWLEDGED' });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async resolveAlert(alertId: string): Promise<Alert> {
|
||||
const response = await this.api.patch(`/monitoring/alerts/${alertId}/`, { status: 'RESOLVED' });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Security endpoints
|
||||
async getAuditLogs(filters?: FilterOptions): Promise<PaginatedResponse<AuditLog>> {
|
||||
const response = await this.api.get('/security/api/audit-logs/', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getRiskAssessments(filters?: FilterOptions): Promise<PaginatedResponse<RiskAssessment>> {
|
||||
const response = await this.api.get('/security/api/risk-assessments/', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async performRiskAssessment(): Promise<RiskAssessment> {
|
||||
const response = await this.api.post('/security/api/zero-trust/assess/');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getDevicePostures(): Promise<DevicePosture[]> {
|
||||
const response = await this.api.get('/security/api/device-postures/');
|
||||
return response.data.results || response.data;
|
||||
}
|
||||
|
||||
async getZeroTrustStatus(): Promise<any> {
|
||||
const response = await this.api.get('/security/api/zero-trust/status/');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Compliance endpoints
|
||||
async getComplianceFrameworks(): Promise<ComplianceFramework[]> {
|
||||
const response = await this.api.get('/compliance/frameworks/');
|
||||
return response.data.results || response.data;
|
||||
}
|
||||
|
||||
// Knowledge Management endpoints
|
||||
async getKnowledgeArticles(filters?: FilterOptions): Promise<PaginatedResponse<KnowledgeArticle>> {
|
||||
const response = await this.api.get('/knowledge/knowledge-articles/', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getPostmortems(filters?: FilterOptions): Promise<PaginatedResponse<Postmortem>> {
|
||||
const response = await this.api.get('/knowledge/postmortems/', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Automation endpoints
|
||||
async getRunbooks(): Promise<Runbook[]> {
|
||||
const response = await this.api.get('/automation/runbooks/');
|
||||
return response.data.results || response.data;
|
||||
}
|
||||
|
||||
async executeRunbook(runbookId: string, incidentId: string): Promise<any> {
|
||||
const response = await this.api.post(`/automation/runbooks/${runbookId}/execute/`, {
|
||||
incident_id: incidentId
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Collaboration endpoints
|
||||
async getWarRooms(filters?: FilterOptions): Promise<PaginatedResponse<WarRoom>> {
|
||||
const response = await this.api.get('/collaboration/war-rooms/', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getWarRoomMessages(warRoomId: string, filters?: FilterOptions): Promise<PaginatedResponse<ChatMessage>> {
|
||||
const response = await this.api.get(`/collaboration/war-rooms/${warRoomId}/messages/`, { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async sendMessage(warRoomId: string, content: string, messageType: string = 'TEXT'): Promise<ChatMessage> {
|
||||
const response = await this.api.post(`/collaboration/war-rooms/${warRoomId}/messages/`, {
|
||||
content,
|
||||
message_type: messageType
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Analytics endpoints
|
||||
async getDashboardData(dashboardId: string): Promise<any> {
|
||||
const response = await this.api.get(`/analytics/dashboard/${dashboardId}/data/`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getKPISummary(): Promise<any> {
|
||||
const response = await this.api.get('/analytics/kpi-metrics/summary/');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getAnomalySummary(): Promise<any> {
|
||||
const response = await this.api.get('/analytics/anomaly-detections/summary/');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getCostSummary(): Promise<any> {
|
||||
const response = await this.api.get('/analytics/cost-analyses/summary/');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// User Management endpoints
|
||||
async getUsersWithFilters(filters?: FilterOptions): Promise<PaginatedResponse<User>> {
|
||||
const response = await this.api.get('/security/api/users/', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getUserById(id: string): Promise<User> {
|
||||
const response = await this.api.get(`/security/api/users/${id}/`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateUser(id: string, data: Partial<User>): Promise<User> {
|
||||
const response = await this.api.patch(`/security/users/${id}/`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Dashboard endpoints
|
||||
async getDashboards(): Promise<Dashboard[]> {
|
||||
const response = await this.api.get('/monitoring/monitoring-dashboards/');
|
||||
return response.data.results || response.data;
|
||||
}
|
||||
|
||||
async getDashboard(id: string): Promise<Dashboard> {
|
||||
const response = await this.api.get(`/monitoring/monitoring-dashboards/${id}/`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Health check endpoints
|
||||
async getHealthStatus(): Promise<any> {
|
||||
const response = await this.api.get('/health/');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getSystemStatus(): Promise<any> {
|
||||
const response = await this.api.get('/monitoring/system-status/');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// User Management endpoints (Admin only)
|
||||
async getUsers(): Promise<User[]> {
|
||||
const response = await this.api.get('/security/api/users/');
|
||||
return response.data.results || response.data;
|
||||
}
|
||||
|
||||
async getUser(id: string): Promise<User> {
|
||||
const response = await this.api.get(`/security/api/users/${id}/`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateUserRoles(userId: string, roleIds: string[]): Promise<void> {
|
||||
await this.api.patch(`/security/api/users/${userId}/update_roles/`, {
|
||||
role_ids: roleIds
|
||||
});
|
||||
}
|
||||
|
||||
async updateUserClearance(userId: string, clearanceLevelId: string): Promise<void> {
|
||||
await this.api.patch(`/security/api/users/${userId}/update_clearance/`, {
|
||||
clearance_level_id: clearanceLevelId
|
||||
});
|
||||
}
|
||||
|
||||
async getDashboardPermissions(): Promise<any> {
|
||||
const response = await this.api.get('/security/api/users/dashboard_permissions/');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getRoles(): Promise<any[]> {
|
||||
const response = await this.api.get('/security/api/roles/');
|
||||
return response.data.results || response.data;
|
||||
}
|
||||
|
||||
async getDataClassifications(): Promise<any[]> {
|
||||
const response = await this.api.get('/security/api/classifications/');
|
||||
return response.data.results || response.data;
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export a singleton instance
|
||||
const apiService = new ApiService();
|
||||
export default apiService;
|
||||
5
etb-dashboard/src/setupTests.ts
Normal file
5
etb-dashboard/src/setupTests.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
496
etb-dashboard/src/types/index.ts
Normal file
496
etb-dashboard/src/types/index.ts
Normal file
@@ -0,0 +1,496 @@
|
||||
// Core types for ETB Dashboard
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
mfa_enabled: boolean;
|
||||
is_superuser: boolean;
|
||||
is_staff: boolean;
|
||||
is_active: boolean;
|
||||
clearance_level: {
|
||||
name: string;
|
||||
level: number;
|
||||
};
|
||||
roles: Role[];
|
||||
department?: string;
|
||||
employee_id?: string;
|
||||
phone_number?: string;
|
||||
emergency_contact?: string;
|
||||
oncall_preferences?: Record<string, any>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: Permission[];
|
||||
data_classification_access: DataClassification[];
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
id: string;
|
||||
name: string;
|
||||
codename: string;
|
||||
content_type: string;
|
||||
}
|
||||
|
||||
export interface DataClassification {
|
||||
id: string;
|
||||
name: string;
|
||||
level: number;
|
||||
description: string;
|
||||
color_code: string;
|
||||
requires_clearance: boolean;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
mfa_token?: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
user: User;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface MFADevice {
|
||||
id: string;
|
||||
name: string;
|
||||
device_type: 'TOTP' | 'HOTP' | 'SMS' | 'EMAIL' | 'HARDWARE';
|
||||
is_active: boolean;
|
||||
is_primary: boolean;
|
||||
last_used?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface MFASetupResponse {
|
||||
device: MFADevice;
|
||||
qr_code_data: string;
|
||||
secret_key: string;
|
||||
}
|
||||
|
||||
export interface Incident {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
free_text: string;
|
||||
category?: string;
|
||||
subcategory?: string;
|
||||
classification_confidence?: number;
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' | 'EMERGENCY';
|
||||
suggested_severity?: string;
|
||||
severity_confidence?: number;
|
||||
priority: 'P1' | 'P2' | 'P3' | 'P4';
|
||||
status: 'OPEN' | 'IN_PROGRESS' | 'RESOLVED' | 'CLOSED' | 'CANCELLED';
|
||||
assigned_to?: User;
|
||||
reporter?: User;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
resolved_at?: string;
|
||||
affected_users: number;
|
||||
business_impact?: string;
|
||||
estimated_downtime?: string;
|
||||
ai_processed: boolean;
|
||||
automation_enabled: boolean;
|
||||
is_duplicate: boolean;
|
||||
data_classification?: DataClassification;
|
||||
security_clearance_required: boolean;
|
||||
is_sensitive: boolean;
|
||||
}
|
||||
|
||||
export interface SLADefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
sla_type: 'RESPONSE_TIME' | 'RESOLUTION_TIME' | 'ACKNOWLEDGMENT_TIME' | 'FIRST_RESPONSE';
|
||||
incident_categories: string[];
|
||||
incident_severities: string[];
|
||||
incident_priorities: string[];
|
||||
target_duration_minutes: number;
|
||||
business_hours_only: boolean;
|
||||
escalation_enabled: boolean;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface SLAInstance {
|
||||
id: string;
|
||||
sla_definition: SLADefinition;
|
||||
incident: Incident;
|
||||
status: 'ACTIVE' | 'MET' | 'BREACHED' | 'CANCELLED';
|
||||
target_time: string;
|
||||
started_at: string;
|
||||
met_at?: string;
|
||||
breached_at?: string;
|
||||
escalation_triggered: boolean;
|
||||
escalation_level: number;
|
||||
response_time?: string;
|
||||
resolution_time?: string;
|
||||
}
|
||||
|
||||
export interface OnCallRotation {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
rotation_type: 'WEEKLY' | 'DAILY' | 'MONTHLY' | 'CUSTOM';
|
||||
status: 'ACTIVE' | 'PAUSED' | 'INACTIVE';
|
||||
team_name: string;
|
||||
timezone: string;
|
||||
external_system: 'PAGERDUTY' | 'OPSGENIE' | 'INTERNAL' | 'CUSTOM';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface OnCallAssignment {
|
||||
id: string;
|
||||
rotation: OnCallRotation;
|
||||
user: User;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
status: 'SCHEDULED' | 'ACTIVE' | 'COMPLETED' | 'CANCELLED';
|
||||
handoff_notes?: string;
|
||||
incidents_handled: number;
|
||||
response_time_avg?: string;
|
||||
}
|
||||
|
||||
export interface MonitoringTarget {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
target_type: 'APPLICATION' | 'DATABASE' | 'CACHE' | 'QUEUE' | 'EXTERNAL_API' | 'SERVICE' | 'INFRASTRUCTURE' | 'MODULE';
|
||||
endpoint_url?: string;
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'MAINTENANCE' | 'ERROR';
|
||||
last_checked?: string;
|
||||
last_status: 'HEALTHY' | 'WARNING' | 'CRITICAL' | 'UNKNOWN';
|
||||
related_module?: string;
|
||||
}
|
||||
|
||||
export interface HealthCheck {
|
||||
id: string;
|
||||
target: MonitoringTarget;
|
||||
check_type: 'HTTP' | 'DATABASE' | 'CACHE' | 'QUEUE' | 'CUSTOM' | 'PING' | 'SSL';
|
||||
status: 'HEALTHY' | 'WARNING' | 'CRITICAL' | 'UNKNOWN';
|
||||
response_time_ms?: number;
|
||||
status_code?: number;
|
||||
error_message?: string;
|
||||
checked_at: string;
|
||||
}
|
||||
|
||||
export interface SystemMetric {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
metric_type: 'PERFORMANCE' | 'BUSINESS' | 'SECURITY' | 'INFRASTRUCTURE' | 'CUSTOM';
|
||||
category: string;
|
||||
unit: string;
|
||||
aggregation_method: 'AVERAGE' | 'SUM' | 'COUNT' | 'MIN' | 'MAX' | 'PERCENTILE_95' | 'PERCENTILE_99';
|
||||
warning_threshold?: number;
|
||||
critical_threshold?: number;
|
||||
is_active: boolean;
|
||||
related_module?: string;
|
||||
}
|
||||
|
||||
export interface MetricMeasurement {
|
||||
id: string;
|
||||
metric: SystemMetric;
|
||||
value: number;
|
||||
timestamp: string;
|
||||
tags: Record<string, any>;
|
||||
metadata: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
status: 'TRIGGERED' | 'ACKNOWLEDGED' | 'RESOLVED' | 'SUPPRESSED';
|
||||
triggered_value?: number;
|
||||
threshold_value?: number;
|
||||
triggered_at: string;
|
||||
acknowledged_at?: string;
|
||||
resolved_at?: string;
|
||||
acknowledged_by?: User;
|
||||
resolved_by?: User;
|
||||
}
|
||||
|
||||
export interface AuditLog {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
user?: User;
|
||||
action_type: string;
|
||||
resource_type?: string;
|
||||
resource_id?: string;
|
||||
ip_address?: string;
|
||||
user_agent?: string;
|
||||
details: Record<string, any>;
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
hash_value: string;
|
||||
}
|
||||
|
||||
export interface RiskAssessment {
|
||||
id: string;
|
||||
user: User;
|
||||
assessment_type: string;
|
||||
device_risk_score: number;
|
||||
location_risk_score: number;
|
||||
behavior_risk_score: number;
|
||||
network_risk_score: number;
|
||||
time_risk_score: number;
|
||||
user_risk_score: number;
|
||||
overall_risk_score: number;
|
||||
risk_level: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
access_decision: 'ALLOW' | 'DENY' | 'STEP_UP' | 'REVIEW';
|
||||
decision_reason: string;
|
||||
assessed_at: string;
|
||||
}
|
||||
|
||||
export interface DevicePosture {
|
||||
id: string;
|
||||
user: User;
|
||||
device_id: string;
|
||||
device_name?: string;
|
||||
device_type: 'DESKTOP' | 'LAPTOP' | 'MOBILE' | 'TABLET' | 'SERVER' | 'IOT' | 'UNKNOWN';
|
||||
os_type: 'WINDOWS' | 'MACOS' | 'LINUX' | 'ANDROID' | 'IOS' | 'UNKNOWN';
|
||||
is_managed: boolean;
|
||||
has_antivirus: boolean;
|
||||
firewall_enabled: boolean;
|
||||
encryption_enabled: boolean;
|
||||
risk_score: number;
|
||||
is_compliant: boolean;
|
||||
is_trusted: boolean;
|
||||
trust_level: 'HIGH' | 'MEDIUM' | 'LOW' | 'UNTRUSTED';
|
||||
last_seen: string;
|
||||
}
|
||||
|
||||
export interface ComplianceFramework {
|
||||
id: string;
|
||||
name: string;
|
||||
framework_type: 'GDPR' | 'HIPAA' | 'SOX' | 'ISO27001' | 'PCI_DSS' | 'NIST' | 'CUSTOM';
|
||||
version: string;
|
||||
description: string;
|
||||
applicable_regions: string[];
|
||||
industry_sectors: string[];
|
||||
compliance_requirements: string[];
|
||||
is_active: boolean;
|
||||
effective_date: string;
|
||||
review_date?: string;
|
||||
}
|
||||
|
||||
export interface KnowledgeArticle {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
author: User;
|
||||
is_published: boolean;
|
||||
view_count: number;
|
||||
rating: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Postmortem {
|
||||
id: string;
|
||||
incident: Incident;
|
||||
title: string;
|
||||
summary: string;
|
||||
timeline: string;
|
||||
root_cause: string;
|
||||
impact: string;
|
||||
lessons_learned: string;
|
||||
action_items: string[];
|
||||
author: User;
|
||||
participants: User[];
|
||||
is_published: boolean;
|
||||
completion_percentage: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Runbook {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
severity_levels: string[];
|
||||
steps: RunbookStep[];
|
||||
is_active: boolean;
|
||||
execution_count: number;
|
||||
success_rate: number;
|
||||
created_by: User;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface RunbookStep {
|
||||
id: string;
|
||||
order: number;
|
||||
title: string;
|
||||
description: string;
|
||||
action_type: 'MANUAL' | 'AUTOMATED' | 'CONDITIONAL';
|
||||
command?: string;
|
||||
expected_result?: string;
|
||||
timeout_seconds?: number;
|
||||
retry_count?: number;
|
||||
is_required: boolean;
|
||||
}
|
||||
|
||||
export interface WarRoom {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
incident: Incident;
|
||||
status: 'ACTIVE' | 'RESOLVED' | 'CLOSED';
|
||||
participants: User[];
|
||||
created_by: User;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
war_room: WarRoom;
|
||||
user: User;
|
||||
content: string;
|
||||
message_type: 'TEXT' | 'SYSTEM' | 'COMMAND' | 'FILE';
|
||||
is_edited: boolean;
|
||||
edited_at?: string;
|
||||
reactions: MessageReaction[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface MessageReaction {
|
||||
id: string;
|
||||
message: ChatMessage;
|
||||
user: User;
|
||||
emoji: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface DashboardWidget {
|
||||
id: string;
|
||||
type: 'CHART' | 'TABLE' | 'METRIC' | 'ALERT' | 'CUSTOM';
|
||||
title: string;
|
||||
config: Record<string, any>;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Dashboard {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
dashboard_type: 'SYSTEM_OVERVIEW' | 'PERFORMANCE' | 'BUSINESS_METRICS' | 'SECURITY' | 'INFRASTRUCTURE' | 'CUSTOM';
|
||||
widgets: DashboardWidget[];
|
||||
is_public: boolean;
|
||||
allowed_users: User[];
|
||||
allowed_roles: string[];
|
||||
auto_refresh_enabled: boolean;
|
||||
refresh_interval_seconds: number;
|
||||
is_active: boolean;
|
||||
created_by: User;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// API Response types
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
message?: string;
|
||||
status: 'success' | 'error';
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
results: T[];
|
||||
count: number;
|
||||
next?: string;
|
||||
previous?: string;
|
||||
}
|
||||
|
||||
// Form types
|
||||
export interface LoginFormData {
|
||||
username: string;
|
||||
password: string;
|
||||
mfa_token?: string;
|
||||
remember_me?: boolean;
|
||||
}
|
||||
|
||||
export interface ChangePasswordFormData {
|
||||
current_password: string;
|
||||
new_password: string;
|
||||
confirm_password: string;
|
||||
}
|
||||
|
||||
export interface CreateIncidentFormData {
|
||||
title: string;
|
||||
description: string;
|
||||
category?: string;
|
||||
severity: string;
|
||||
priority: string;
|
||||
assigned_to?: string;
|
||||
business_impact?: string;
|
||||
affected_users: number;
|
||||
}
|
||||
|
||||
// Navigation types
|
||||
export interface NavigationItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
path: string;
|
||||
children?: NavigationItem[];
|
||||
permissions?: string[];
|
||||
clearance_level?: number;
|
||||
}
|
||||
|
||||
// Theme types
|
||||
export interface ThemeConfig {
|
||||
mode: 'light' | 'dark';
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
fontFamily: string;
|
||||
}
|
||||
|
||||
// Error types
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
code?: string;
|
||||
details?: Record<string, any>;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
// Loading states
|
||||
export interface LoadingState {
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
// Filter types
|
||||
export interface FilterOptions {
|
||||
search?: string;
|
||||
category?: string;
|
||||
severity?: string;
|
||||
status?: string;
|
||||
assigned_to?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
sort_by?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
63
etb-dashboard/tailwind.config.js
Normal file
63
etb-dashboard/tailwind.config.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'etb-blue': {
|
||||
50: '#f0f4ff',
|
||||
100: '#e0e9ff',
|
||||
200: '#c7d7ff',
|
||||
300: '#a5b8ff',
|
||||
400: '#8190ff',
|
||||
500: '#1e3c72',
|
||||
600: '#2a5298',
|
||||
700: '#1a2f5c',
|
||||
800: '#142547',
|
||||
900: '#0f1b33',
|
||||
},
|
||||
'etb-gray': {
|
||||
50: '#f8fafc',
|
||||
100: '#f1f5f9',
|
||||
200: '#e2e8f0',
|
||||
300: '#cbd5e1',
|
||||
400: '#94a3b8',
|
||||
500: '#64748b',
|
||||
600: '#475569',
|
||||
700: '#334155',
|
||||
800: '#1e293b',
|
||||
900: '#0f172a',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
'inter': ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 1s ease-in-out',
|
||||
'slide-up': 'slideUp 0.8s ease-out',
|
||||
'pulse-glow': 'pulseGlow 2s infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0', transform: 'translateY(20px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { opacity: '0', transform: 'translateY(30px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
pulseGlow: {
|
||||
'0%': { boxShadow: '0 0 0 0 rgba(30, 60, 114, 0.4)' },
|
||||
'70%': { boxShadow: '0 0 0 10px rgba(30, 60, 114, 0)' },
|
||||
'100%': { boxShadow: '0 0 0 0 rgba(30, 60, 114, 0)' },
|
||||
},
|
||||
},
|
||||
backdropBlur: {
|
||||
'xs': '2px',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
26
etb-dashboard/tsconfig.json
Normal file
26
etb-dashboard/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user