Update README and documentation; refactor frontend components for improved structure and resilience
This commit is contained in:
@@ -1,136 +1,59 @@
|
||||
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 { Row, Col, Typography, Alert } from 'antd';
|
||||
import SystemMetrics from './SystemMetrics';
|
||||
import ServiceStatusBanner from './ServiceStatusBanner';
|
||||
import SystemStatsCards from './dashboard/SystemStatsCards';
|
||||
import ServiceStatusList from './dashboard/ServiceStatusList';
|
||||
import RecentEventsList from './dashboard/RecentEventsList';
|
||||
import LoadingSpinner from './common/LoadingSpinner';
|
||||
import { useServiceStatus, useSystemData } from '../hooks/useServiceStatus';
|
||||
import { ERROR_MESSAGES } from '../constants';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Title } = 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 serviceStatus = useServiceStatus();
|
||||
const { systemStats, services, events: recentEvents, loading, error } = useSystemData();
|
||||
|
||||
const handleRefresh = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
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' }} />;
|
||||
};
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="dashboard-container">
|
||||
<LoadingSpinner message="Loading dashboard..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard-container">
|
||||
<ServiceStatusBanner serviceStatus={serviceStatus} onRefresh={handleRefresh} />
|
||||
|
||||
<Title level={2}>System Overview</Title>
|
||||
|
||||
{error && (
|
||||
<Alert
|
||||
message={ERROR_MESSAGES.DATA_LOADING_ERROR}
|
||||
description={error}
|
||||
type="warning"
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
<SystemStatsCards systemStats={systemStats} />
|
||||
|
||||
<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>
|
||||
<ServiceStatusList services={services} />
|
||||
</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>
|
||||
<RecentEventsList events={recentEvents} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
41
frontend/src/components/OfflineMode.js
Normal file
41
frontend/src/components/OfflineMode.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button, Space } from 'antd';
|
||||
import { WifiOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
|
||||
const OfflineMode = ({ onRetry }) => {
|
||||
return (
|
||||
<Alert
|
||||
message="Offline Mode"
|
||||
description={
|
||||
<div>
|
||||
<p>The frontend is running in offline mode because backend services are not available.</p>
|
||||
<p>To enable full functionality:</p>
|
||||
<ol style={{ margin: '8px 0', paddingLeft: '20px' }}>
|
||||
<li>Start the backend services: <code>docker-compose up -d</code></li>
|
||||
<li>Or start individual services for development</li>
|
||||
<li>Refresh this page once services are running</li>
|
||||
</ol>
|
||||
<Space style={{ marginTop: 12 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={onRetry}
|
||||
>
|
||||
Retry Connection
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => window.open('http://localhost:8083', '_blank')}
|
||||
>
|
||||
Check API Documentation
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OfflineMode;
|
||||
103
frontend/src/components/ServiceStatusBanner.js
Normal file
103
frontend/src/components/ServiceStatusBanner.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button, Space } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import StatusIcon from './common/StatusIcon';
|
||||
import { UI_CONSTANTS } from '../constants';
|
||||
|
||||
const ServiceStatusBanner = ({ serviceStatus, onRefresh }) => {
|
||||
|
||||
const getStatusMessage = () => {
|
||||
switch (serviceStatus.overall) {
|
||||
case 'online':
|
||||
return 'All services are running normally';
|
||||
case 'partial':
|
||||
return 'Some services are unavailable - limited functionality';
|
||||
case 'offline':
|
||||
return 'Backend services are offline - running in offline mode';
|
||||
default:
|
||||
return 'Checking service status...';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusType = () => {
|
||||
switch (serviceStatus.overall) {
|
||||
case 'online':
|
||||
return 'success';
|
||||
case 'partial':
|
||||
return 'warning';
|
||||
case 'offline':
|
||||
return 'error';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const getServiceDetails = () => {
|
||||
const details = [];
|
||||
|
||||
if (!serviceStatus.apiGateway.available) {
|
||||
details.push(`API Gateway: ${serviceStatus.apiGateway.error || 'Unavailable'}`);
|
||||
}
|
||||
if (!serviceStatus.serviceAdapters.available) {
|
||||
details.push(`Service Adapters: ${serviceStatus.serviceAdapters.error || 'Unavailable'}`);
|
||||
}
|
||||
if (!serviceStatus.apiDocs.available) {
|
||||
details.push(`API Docs: ${serviceStatus.apiDocs.error || 'Unavailable'}`);
|
||||
}
|
||||
|
||||
return details;
|
||||
};
|
||||
|
||||
if (serviceStatus.overall === 'online') {
|
||||
return null; // Don't show banner when everything is working
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
message={
|
||||
<Space>
|
||||
<StatusIcon status={serviceStatus.overall} />
|
||||
<span>{getStatusMessage()}</span>
|
||||
{onRefresh && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={onRefresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
serviceStatus.overall !== 'checking' && (
|
||||
<div>
|
||||
{getServiceDetails().length > 0 && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<strong>Service Details:</strong>
|
||||
<ul style={{ margin: '4px 0 0 0', paddingLeft: '20px' }}>
|
||||
{getServiceDetails().map((detail, index) => (
|
||||
<li key={index}>{detail}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{serviceStatus.overall === 'offline' && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<strong>Offline Mode:</strong> The frontend is running with fallback data.
|
||||
Start the backend services to enable full functionality.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
type={getStatusType()}
|
||||
showIcon={false}
|
||||
style={{ marginBottom: UI_CONSTANTS.MARGIN_BOTTOM }}
|
||||
closable={serviceStatus.overall === 'partial'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceStatusBanner;
|
||||
@@ -1,9 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Card, Row, Col, Statistic, Progress } from 'antd';
|
||||
import { Card, Row, Col, Statistic, Progress, Alert } from 'antd';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
|
||||
import { useSystemData } from '../hooks/useServiceStatus';
|
||||
|
||||
const SystemMetrics = () => {
|
||||
// Mock data for charts
|
||||
const { systemStats, loading, error } = useSystemData();
|
||||
|
||||
// Mock data for charts (fallback when services are unavailable)
|
||||
const cpuData = [
|
||||
{ time: '00:00', cpu: 25 },
|
||||
{ time: '04:00', cpu: 30 },
|
||||
@@ -34,26 +37,45 @@ const SystemMetrics = () => {
|
||||
{ time: '24:00', in: 6, out: 4 }
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card title="System Performance Metrics">
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
Loading metrics...
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error && (
|
||||
<Alert
|
||||
message="Metrics Unavailable"
|
||||
description="Real-time metrics are not available. Showing sample data."
|
||||
type="warning"
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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} />
|
||||
<Statistic title="CPU Usage (24h)" value={systemStats.cpu || 0} suffix="%" />
|
||||
<Progress percent={systemStats.cpu || 0} 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} />
|
||||
<Statistic title="Memory Usage (24h)" value={systemStats.memory || 0} suffix="%" />
|
||||
<Progress percent={systemStats.memory || 0} showInfo={false} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small">
|
||||
<Statistic title="Disk Usage" value={32.1} suffix="%" />
|
||||
<Progress percent={32.1} showInfo={false} />
|
||||
<Statistic title="Disk Usage" value={systemStats.disk || 0} suffix="%" />
|
||||
<Progress percent={systemStats.disk || 0} showInfo={false} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
76
frontend/src/components/common/ErrorBoundary.js
Normal file
76
frontend/src/components/common/ErrorBoundary.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Button } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, errorInfo: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo
|
||||
});
|
||||
|
||||
// Log error to console in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
handleReload = () => {
|
||||
this.setState({ hasError: false, error: null, errorInfo: null });
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{ padding: '24px', textAlign: 'center' }}>
|
||||
<Alert
|
||||
message="Something went wrong"
|
||||
description={
|
||||
<div>
|
||||
<p>The application encountered an unexpected error.</p>
|
||||
{process.env.NODE_ENV === 'development' && this.state.error && (
|
||||
<details style={{ marginTop: '16px', textAlign: 'left' }}>
|
||||
<summary>Error Details (Development)</summary>
|
||||
<pre style={{ marginTop: '8px', fontSize: '12px' }}>
|
||||
{this.state.error.toString()}
|
||||
{this.state.errorInfo.componentStack}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={this.handleReload}
|
||||
style={{ marginTop: '16px' }}
|
||||
>
|
||||
Reload Page
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
ErrorBoundary.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
export default ErrorBoundary;
|
||||
34
frontend/src/components/common/LoadingSpinner.js
Normal file
34
frontend/src/components/common/LoadingSpinner.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Spin } from 'antd';
|
||||
import { UI_CONSTANTS } from '../../constants';
|
||||
|
||||
const LoadingSpinner = ({
|
||||
message = 'Loading...',
|
||||
size = UI_CONSTANTS.SPINNER_SIZE,
|
||||
centered = true
|
||||
}) => {
|
||||
const containerStyle = centered ? {
|
||||
textAlign: 'center',
|
||||
padding: `${UI_CONSTANTS.PADDING.LARGE}px`
|
||||
} : {};
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<Spin size={size} />
|
||||
{message && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LoadingSpinner.propTypes = {
|
||||
message: PropTypes.string,
|
||||
size: PropTypes.oneOf(['small', 'default', 'large']),
|
||||
centered: PropTypes.bool
|
||||
};
|
||||
|
||||
export default LoadingSpinner;
|
||||
48
frontend/src/components/common/StatusIcon.js
Normal file
48
frontend/src/components/common/StatusIcon.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
CloseCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { COLORS } from '../../constants';
|
||||
|
||||
const StatusIcon = ({ status, size = 'default' }) => {
|
||||
const iconProps = {
|
||||
style: {
|
||||
color: getStatusColor(status),
|
||||
fontSize: size === 'large' ? '20px' : '16px'
|
||||
}
|
||||
};
|
||||
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return <CheckCircleOutlined {...iconProps} />;
|
||||
case 'partial':
|
||||
return <ExclamationCircleOutlined {...iconProps} />;
|
||||
case 'offline':
|
||||
return <CloseCircleOutlined {...iconProps} />;
|
||||
default:
|
||||
return <ExclamationCircleOutlined {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return COLORS.SUCCESS;
|
||||
case 'partial':
|
||||
return COLORS.WARNING;
|
||||
case 'offline':
|
||||
return COLORS.ERROR;
|
||||
default:
|
||||
return COLORS.DISABLED;
|
||||
}
|
||||
};
|
||||
|
||||
StatusIcon.propTypes = {
|
||||
status: PropTypes.oneOf(['online', 'partial', 'offline', 'checking']).isRequired,
|
||||
size: PropTypes.oneOf(['default', 'large'])
|
||||
};
|
||||
|
||||
export default StatusIcon;
|
||||
34
frontend/src/components/dashboard/RecentEventsList.js
Normal file
34
frontend/src/components/dashboard/RecentEventsList.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Card, List } from 'antd';
|
||||
import { UI_CONSTANTS } from '../../constants';
|
||||
|
||||
const RecentEventsList = ({ events }) => {
|
||||
const renderEventItem = (event) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={event.event}
|
||||
description={`${event.time} • ${event.service}`}
|
||||
/>
|
||||
</List.Item>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card title="Recent Events" style={{ height: UI_CONSTANTS.CARD_HEIGHT }}>
|
||||
<List
|
||||
dataSource={events}
|
||||
renderItem={renderEventItem}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
RecentEventsList.propTypes = {
|
||||
events: PropTypes.arrayOf(PropTypes.shape({
|
||||
time: PropTypes.string.isRequired,
|
||||
event: PropTypes.string.isRequired,
|
||||
service: PropTypes.string.isRequired
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default RecentEventsList;
|
||||
41
frontend/src/components/dashboard/ServiceStatusList.js
Normal file
41
frontend/src/components/dashboard/ServiceStatusList.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Card, List, Typography } from 'antd';
|
||||
import StatusIcon from '../common/StatusIcon';
|
||||
import { UI_CONSTANTS } from '../../constants';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const ServiceStatusList = ({ services }) => {
|
||||
const renderServiceItem = (service) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={<StatusIcon status={service.status} />}
|
||||
title={service.name}
|
||||
description={`Uptime: ${service.uptime}`}
|
||||
/>
|
||||
<Text type={service.status === 'online' ? 'success' : 'danger'}>
|
||||
{service.status.toUpperCase()}
|
||||
</Text>
|
||||
</List.Item>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card title="Service Status" style={{ height: UI_CONSTANTS.CARD_HEIGHT }}>
|
||||
<List
|
||||
dataSource={services}
|
||||
renderItem={renderServiceItem}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
ServiceStatusList.propTypes = {
|
||||
services: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
status: PropTypes.oneOf(['online', 'offline']).isRequired,
|
||||
uptime: PropTypes.string.isRequired
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default ServiceStatusList;
|
||||
76
frontend/src/components/dashboard/SystemStatsCards.js
Normal file
76
frontend/src/components/dashboard/SystemStatsCards.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Row, Col, Card, Statistic, Progress } from 'antd';
|
||||
import {
|
||||
ServerOutlined,
|
||||
DatabaseOutlined,
|
||||
WifiOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { UI_CONSTANTS } from '../../constants';
|
||||
|
||||
const SystemStatsCards = ({ systemStats }) => {
|
||||
const stats = [
|
||||
{
|
||||
key: 'cpu',
|
||||
title: 'CPU Usage',
|
||||
value: systemStats.cpu || 0,
|
||||
suffix: '%',
|
||||
prefix: <ServerOutlined />
|
||||
},
|
||||
{
|
||||
key: 'memory',
|
||||
title: 'Memory Usage',
|
||||
value: systemStats.memory || 0,
|
||||
suffix: '%',
|
||||
prefix: <DatabaseOutlined />
|
||||
},
|
||||
{
|
||||
key: 'disk',
|
||||
title: 'Disk Usage',
|
||||
value: systemStats.disk || 0,
|
||||
suffix: '%',
|
||||
prefix: <DatabaseOutlined />
|
||||
},
|
||||
{
|
||||
key: 'network',
|
||||
title: 'Network',
|
||||
value: systemStats.network || 0,
|
||||
suffix: 'Mbps',
|
||||
prefix: <WifiOutlined />
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Row gutter={16} style={{ marginBottom: UI_CONSTANTS.MARGIN_TOP }}>
|
||||
{stats.map((stat) => (
|
||||
<Col span={6} key={stat.key}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={stat.title}
|
||||
value={stat.value}
|
||||
suffix={stat.suffix}
|
||||
prefix={stat.prefix}
|
||||
/>
|
||||
{stat.suffix === '%' && (
|
||||
<Progress
|
||||
percent={stat.value}
|
||||
showInfo={false}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
SystemStatsCards.propTypes = {
|
||||
systemStats: PropTypes.shape({
|
||||
cpu: PropTypes.number,
|
||||
memory: PropTypes.number,
|
||||
disk: PropTypes.number,
|
||||
network: PropTypes.number
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default SystemStatsCards;
|
||||
Reference in New Issue
Block a user