initial project setup
This commit is contained in:
24
frontend/Dockerfile
Normal file
24
frontend/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Install serve to run the app
|
||||
RUN npm install -g serve
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application
|
||||
CMD ["serve", "-s", "build", "-l", "3000"]
|
||||
18
frontend/Dockerfile.dev
Normal file
18
frontend/Dockerfile.dev
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Run in development mode with hot reload
|
||||
CMD ["npm", "start"]
|
||||
50
frontend/package.json
Normal file
50
frontend/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "labfusion-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "LabFusion Dashboard Frontend",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"axios": "^1.6.2",
|
||||
"recharts": "^2.8.0",
|
||||
"antd": "^5.12.8",
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"styled-components": "^6.1.6",
|
||||
"react-query": "^3.39.3",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"lodash": "^4.17.21",
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:8080"
|
||||
}
|
||||
20
frontend/public/index.html
Normal file
20
frontend/public/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!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="LabFusion - Unified homelab dashboard and integration hub"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>LabFusion Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
111
frontend/src/App.css
Normal file
111
frontend/src/App.css
Normal file
@@ -0,0 +1,111 @@
|
||||
.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;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
background-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.status-unknown {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
64
frontend/src/App.js
Normal file
64
frontend/src/App.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { Layout, Menu, Typography } from 'antd';
|
||||
import { DashboardOutlined, SettingOutlined, BarChartOutlined } from '@ant-design/icons';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import SystemMetrics from './components/SystemMetrics';
|
||||
import Settings from './components/Settings';
|
||||
import './App.css';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
const { Title } = Typography;
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sider width={250} theme="dark">
|
||||
<div style={{ padding: '16px', textAlign: 'center' }}>
|
||||
<Title level={3} style={{ color: 'white', margin: 0 }}>
|
||||
LabFusion
|
||||
</Title>
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
defaultSelectedKeys={['dashboard']}
|
||||
items={[
|
||||
{
|
||||
key: 'dashboard',
|
||||
icon: <DashboardOutlined />,
|
||||
label: 'Dashboard',
|
||||
},
|
||||
{
|
||||
key: 'metrics',
|
||||
icon: <BarChartOutlined />,
|
||||
label: 'System Metrics',
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
icon: <SettingOutlined />,
|
||||
label: 'Settings',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header style={{ background: '#fff', padding: '0 24px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
|
||||
<Title level={4} style={{ margin: 0, lineHeight: '64px' }}>
|
||||
Homelab Dashboard
|
||||
</Title>
|
||||
</Header>
|
||||
<Content style={{ margin: '24px', background: '#fff', borderRadius: '8px' }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/metrics" element={<SystemMetrics />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
147
frontend/src/components/Dashboard.js
Normal file
147
frontend/src/components/Dashboard.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import React from 'react';
|
||||
import { Row, Col, Card, Statistic, Progress, List, Typography } from 'antd';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
ServerOutlined,
|
||||
DatabaseOutlined,
|
||||
WifiOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
import SystemMetrics from './SystemMetrics';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const Dashboard = () => {
|
||||
// Mock data - in real app, this would come from API
|
||||
const systemStats = {
|
||||
cpu: 45.2,
|
||||
memory: 68.5,
|
||||
disk: 32.1,
|
||||
network: 12.3
|
||||
};
|
||||
|
||||
const services = [
|
||||
{ name: 'Home Assistant', status: 'online', uptime: '7d 12h' },
|
||||
{ name: 'Frigate', status: 'online', uptime: '7d 12h' },
|
||||
{ name: 'Immich', status: 'online', uptime: '7d 12h' },
|
||||
{ name: 'n8n', status: 'offline', uptime: '0d 0h' },
|
||||
{ name: 'PostgreSQL', status: 'online', uptime: '7d 12h' },
|
||||
{ name: 'Redis', status: 'online', uptime: '7d 12h' }
|
||||
];
|
||||
|
||||
const recentEvents = [
|
||||
{ time: '2 minutes ago', event: 'Person detected at front door', service: 'Frigate' },
|
||||
{ time: '5 minutes ago', event: 'CPU usage above 80%', service: 'System' },
|
||||
{ time: '12 minutes ago', event: 'Alice arrived home', service: 'Home Assistant' },
|
||||
{ time: '1 hour ago', event: 'New photo uploaded', service: 'Immich' }
|
||||
];
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
return status === 'online' ?
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} /> :
|
||||
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dashboard-container">
|
||||
<Title level={2}>System Overview</Title>
|
||||
|
||||
{/* System Metrics */}
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="CPU Usage"
|
||||
value={systemStats.cpu}
|
||||
suffix="%"
|
||||
prefix={<ServerOutlined />}
|
||||
/>
|
||||
<Progress percent={systemStats.cpu} showInfo={false} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Memory Usage"
|
||||
value={systemStats.memory}
|
||||
suffix="%"
|
||||
prefix={<DatabaseOutlined />}
|
||||
/>
|
||||
<Progress percent={systemStats.memory} showInfo={false} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Disk Usage"
|
||||
value={systemStats.disk}
|
||||
suffix="%"
|
||||
prefix={<DatabaseOutlined />}
|
||||
/>
|
||||
<Progress percent={systemStats.disk} showInfo={false} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Network"
|
||||
value={systemStats.network}
|
||||
suffix="Mbps"
|
||||
prefix={<WifiOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
{/* Service Status */}
|
||||
<Col span={12}>
|
||||
<Card title="Service Status" style={{ height: 400 }}>
|
||||
<List
|
||||
dataSource={services}
|
||||
renderItem={(service) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={getStatusIcon(service.status)}
|
||||
title={service.name}
|
||||
description={`Uptime: ${service.uptime}`}
|
||||
/>
|
||||
<Text type={service.status === 'online' ? 'success' : 'danger'}>
|
||||
{service.status.toUpperCase()}
|
||||
</Text>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Recent Events */}
|
||||
<Col span={12}>
|
||||
<Card title="Recent Events" style={{ height: 400 }}>
|
||||
<List
|
||||
dataSource={recentEvents}
|
||||
renderItem={(event) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={event.event}
|
||||
description={`${event.time} • ${event.service}`}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* System Metrics Chart */}
|
||||
<Row style={{ marginTop: 24 }}>
|
||||
<Col span={24}>
|
||||
<SystemMetrics />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
124
frontend/src/components/Settings.js
Normal file
124
frontend/src/components/Settings.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Form, Input, Button, Switch, Select, Divider, Typography, message } from 'antd';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const Settings = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onFinish = (values) => {
|
||||
setLoading(true);
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
message.success('Settings saved successfully!');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dashboard-container">
|
||||
<Title level={2}>Settings</Title>
|
||||
|
||||
<Card title="Service Integrations" style={{ marginBottom: 24 }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
initialValues={{
|
||||
homeAssistant: {
|
||||
enabled: true,
|
||||
url: 'http://homeassistant.local:8123',
|
||||
token: 'your-token-here'
|
||||
},
|
||||
frigate: {
|
||||
enabled: true,
|
||||
url: 'http://frigate.local:5000',
|
||||
token: 'your-token-here'
|
||||
},
|
||||
immich: {
|
||||
enabled: false,
|
||||
url: 'http://immich.local:2283',
|
||||
apiKey: 'your-api-key-here'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Home Assistant */}
|
||||
<Card size="small" title="Home Assistant" style={{ marginBottom: 16 }}>
|
||||
<Form.Item name={['homeAssistant', 'enabled']} valuePropName="checked">
|
||||
<Switch checkedChildren="Enabled" unCheckedChildren="Disabled" />
|
||||
</Form.Item>
|
||||
<Form.Item label="URL" name={['homeAssistant', 'url']}>
|
||||
<Input placeholder="http://homeassistant.local:8123" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Token" name={['homeAssistant', 'token']}>
|
||||
<Input.Password placeholder="Your Home Assistant token" />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
{/* Frigate */}
|
||||
<Card size="small" title="Frigate" style={{ marginBottom: 16 }}>
|
||||
<Form.Item name={['frigate', 'enabled']} valuePropName="checked">
|
||||
<Switch checkedChildren="Enabled" unCheckedChildren="Disabled" />
|
||||
</Form.Item>
|
||||
<Form.Item label="URL" name={['frigate', 'url']}>
|
||||
<Input placeholder="http://frigate.local:5000" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Token" name={['frigate', 'token']}>
|
||||
<Input.Password placeholder="Your Frigate token" />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
{/* Immich */}
|
||||
<Card size="small" title="Immich" style={{ marginBottom: 16 }}>
|
||||
<Form.Item name={['immich', 'enabled']} valuePropName="checked">
|
||||
<Switch checkedChildren="Enabled" unCheckedChildren="Disabled" />
|
||||
</Form.Item>
|
||||
<Form.Item label="URL" name={['immich', 'url']}>
|
||||
<Input placeholder="http://immich.local:2283" />
|
||||
</Form.Item>
|
||||
<Form.Item label="API Key" name={['immich', 'apiKey']}>
|
||||
<Input.Password placeholder="Your Immich API key" />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
Save Settings
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card title="Dashboard Configuration">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="Default Dashboard Layout">
|
||||
<Select defaultValue="grid" style={{ width: 200 }}>
|
||||
<Option value="grid">Grid Layout</Option>
|
||||
<Option value="list">List Layout</Option>
|
||||
<Option value="custom">Custom Layout</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Auto-refresh Interval">
|
||||
<Select defaultValue="30" style={{ width: 200 }}>
|
||||
<Option value="10">10 seconds</Option>
|
||||
<Option value="30">30 seconds</Option>
|
||||
<Option value="60">1 minute</Option>
|
||||
<Option value="300">5 minutes</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Theme">
|
||||
<Select defaultValue="light" style={{ width: 200 }}>
|
||||
<Option value="light">Light</Option>
|
||||
<Option value="dark">Dark</Option>
|
||||
<Option value="auto">Auto</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
111
frontend/src/components/SystemMetrics.js
Normal file
111
frontend/src/components/SystemMetrics.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { Card, Row, Col, Statistic, Progress } from 'antd';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
|
||||
|
||||
const SystemMetrics = () => {
|
||||
// Mock data for charts
|
||||
const cpuData = [
|
||||
{ time: '00:00', cpu: 25 },
|
||||
{ time: '04:00', cpu: 30 },
|
||||
{ time: '08:00', cpu: 45 },
|
||||
{ time: '12:00', cpu: 60 },
|
||||
{ time: '16:00', cpu: 55 },
|
||||
{ time: '20:00', cpu: 40 },
|
||||
{ time: '24:00', cpu: 35 }
|
||||
];
|
||||
|
||||
const memoryData = [
|
||||
{ time: '00:00', memory: 2.1 },
|
||||
{ time: '04:00', memory: 2.3 },
|
||||
{ time: '08:00', memory: 2.8 },
|
||||
{ time: '12:00', memory: 3.2 },
|
||||
{ time: '16:00', memory: 3.0 },
|
||||
{ time: '20:00', memory: 2.7 },
|
||||
{ time: '24:00', memory: 2.4 }
|
||||
];
|
||||
|
||||
const networkData = [
|
||||
{ time: '00:00', in: 5, out: 3 },
|
||||
{ time: '04:00', in: 8, out: 4 },
|
||||
{ time: '08:00', in: 15, out: 8 },
|
||||
{ time: '12:00', in: 20, out: 12 },
|
||||
{ time: '16:00', in: 18, out: 10 },
|
||||
{ time: '20:00', in: 12, out: 7 },
|
||||
{ time: '24:00', in: 6, out: 4 }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card title="System Performance Metrics" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Card size="small">
|
||||
<Statistic title="CPU Usage (24h)" value={45.2} suffix="%" />
|
||||
<Progress percent={45.2} showInfo={false} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small">
|
||||
<Statistic title="Memory Usage (24h)" value={68.5} suffix="%" />
|
||||
<Progress percent={68.5} showInfo={false} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small">
|
||||
<Statistic title="Disk Usage" value={32.1} suffix="%" />
|
||||
<Progress percent={32.1} showInfo={false} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card title="CPU Usage Over Time">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={cpuData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="time" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Area type="monotone" dataKey="cpu" stroke="#1890ff" fill="#1890ff" fillOpacity={0.3} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title="Memory Usage Over Time">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={memoryData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="time" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="memory" stroke="#52c41a" strokeWidth={2} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16} style={{ marginTop: 16 }}>
|
||||
<Col span={24}>
|
||||
<Card title="Network Traffic">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={networkData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="time" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Area type="monotone" dataKey="in" stackId="1" stroke="#1890ff" fill="#1890ff" fillOpacity={0.6} />
|
||||
<Area type="monotone" dataKey="out" stackId="1" stroke="#52c41a" fill="#52c41a" fillOpacity={0.6} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemMetrics;
|
||||
70
frontend/src/index.css
Normal file
70
frontend/src/index.css
Normal file
@@ -0,0 +1,70 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
padding: 24px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.widget-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
background-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.status-unknown {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
29
frontend/src/index.js
Normal file
29
frontend/src/index.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: '#1890ff',
|
||||
borderRadius: 6,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</ConfigProvider>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
Reference in New Issue
Block a user