Update README and documentation; refactor frontend components for improved structure and resilience
This commit is contained in:
@@ -5,59 +5,73 @@ import { DashboardOutlined, SettingOutlined, BarChartOutlined } from '@ant-desig
|
||||
import Dashboard from './components/Dashboard';
|
||||
import SystemMetrics from './components/SystemMetrics';
|
||||
import Settings from './components/Settings';
|
||||
import OfflineMode from './components/OfflineMode';
|
||||
import ErrorBoundary from './components/common/ErrorBoundary';
|
||||
import { useServiceStatus } from './hooks/useServiceStatus';
|
||||
import './App.css';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
const { Title } = Typography;
|
||||
|
||||
function App() {
|
||||
const serviceStatus = useServiceStatus();
|
||||
|
||||
const handleRetry = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
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>
|
||||
<ErrorBoundary>
|
||||
<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' }}>
|
||||
{serviceStatus.overall === 'offline' && (
|
||||
<OfflineMode onRetry={handleRetry} />
|
||||
)}
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/metrics" element={<SystemMetrics />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
76
frontend/src/constants/index.js
Normal file
76
frontend/src/constants/index.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// API Configuration
|
||||
export const API_CONFIG = {
|
||||
TIMEOUT: 5000,
|
||||
RETRY_ATTEMPTS: 3,
|
||||
REFRESH_INTERVALS: {
|
||||
SERVICE_STATUS: 30000, // 30 seconds
|
||||
SYSTEM_DATA: 60000, // 60 seconds
|
||||
}
|
||||
};
|
||||
|
||||
// Service URLs
|
||||
export const SERVICE_URLS = {
|
||||
API_GATEWAY: process.env.REACT_APP_API_URL || 'http://localhost:8080',
|
||||
SERVICE_ADAPTERS: process.env.REACT_APP_ADAPTERS_URL || 'http://localhost:8000',
|
||||
API_DOCS: process.env.REACT_APP_DOCS_URL || 'http://localhost:8083',
|
||||
};
|
||||
|
||||
// Service Status Types
|
||||
export const SERVICE_STATUS = {
|
||||
ONLINE: 'online',
|
||||
OFFLINE: 'offline',
|
||||
PARTIAL: 'partial',
|
||||
CHECKING: 'checking'
|
||||
};
|
||||
|
||||
// UI Constants
|
||||
export const UI_CONSTANTS = {
|
||||
CARD_HEIGHT: 400,
|
||||
SPINNER_SIZE: 'large',
|
||||
CHART_HEIGHT: 300,
|
||||
MARGIN_BOTTOM: 16,
|
||||
MARGIN_TOP: 24,
|
||||
PADDING: {
|
||||
SMALL: 16,
|
||||
MEDIUM: 24,
|
||||
LARGE: 50
|
||||
}
|
||||
};
|
||||
|
||||
// Colors
|
||||
export const COLORS = {
|
||||
SUCCESS: '#52c41a',
|
||||
WARNING: '#faad14',
|
||||
ERROR: '#ff4d4f',
|
||||
INFO: '#1890ff',
|
||||
DISABLED: '#d9d9d9'
|
||||
};
|
||||
|
||||
// Error Messages
|
||||
export const ERROR_MESSAGES = {
|
||||
CONNECTION_TIMEOUT: 'Request timeout - service may be unavailable',
|
||||
SERVICE_ERROR: 'Service error',
|
||||
SERVICE_UNAVAILABLE: 'Service unavailable - check if backend is running',
|
||||
UNKNOWN_ERROR: 'Unknown error occurred',
|
||||
DATA_LOADING_ERROR: 'Data Loading Error',
|
||||
METRICS_UNAVAILABLE: 'Metrics Unavailable'
|
||||
};
|
||||
|
||||
// Fallback Data
|
||||
export const FALLBACK_DATA = {
|
||||
SYSTEM_STATS: {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
disk: 0,
|
||||
network: 0
|
||||
},
|
||||
SERVICES: [
|
||||
{ name: 'API Gateway', status: 'offline', uptime: '0d 0h' },
|
||||
{ name: 'Service Adapters', status: 'offline', uptime: '0d 0h' },
|
||||
{ name: 'PostgreSQL', status: 'offline', uptime: '0d 0h' },
|
||||
{ name: 'Redis', status: 'offline', uptime: '0d 0h' }
|
||||
],
|
||||
EVENTS: [
|
||||
{ time: 'Service unavailable', event: 'Backend services are not running', service: 'System' }
|
||||
]
|
||||
};
|
||||
129
frontend/src/hooks/useServiceStatus.js
Normal file
129
frontend/src/hooks/useServiceStatus.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { apiGateway, serviceAdapters, apiDocs, fallbackData } from '../services/api';
|
||||
import { API_CONFIG, SERVICE_STATUS } from '../constants';
|
||||
import { determineServiceStatus, formatServiceData, formatEventData } from '../utils/errorHandling';
|
||||
|
||||
export const useServiceStatus = () => {
|
||||
const [status, setStatus] = useState({
|
||||
loading: true,
|
||||
apiGateway: { available: false, error: null },
|
||||
serviceAdapters: { available: false, error: null },
|
||||
apiDocs: { available: false, error: null },
|
||||
overall: SERVICE_STATUS.CHECKING
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const checkServices = async () => {
|
||||
setStatus(prev => ({ ...prev, loading: true }));
|
||||
|
||||
// Check all services in parallel
|
||||
const [apiGatewayResult, adaptersResult, docsResult] = await Promise.allSettled([
|
||||
apiGateway.health(),
|
||||
serviceAdapters.health(),
|
||||
apiDocs.health()
|
||||
]);
|
||||
|
||||
const newStatus = {
|
||||
loading: false,
|
||||
apiGateway: {
|
||||
available: apiGatewayResult.status === 'fulfilled' && apiGatewayResult.value.success,
|
||||
error: apiGatewayResult.status === 'rejected' ? 'Connection failed' :
|
||||
(apiGatewayResult.value?.error || null)
|
||||
},
|
||||
serviceAdapters: {
|
||||
available: adaptersResult.status === 'fulfilled' && adaptersResult.value.success,
|
||||
error: adaptersResult.status === 'rejected' ? 'Connection failed' :
|
||||
(adaptersResult.value?.error || null)
|
||||
},
|
||||
apiDocs: {
|
||||
available: docsResult.status === 'fulfilled' && docsResult.value.success,
|
||||
error: docsResult.status === 'rejected' ? 'Connection failed' :
|
||||
(docsResult.value?.error || null)
|
||||
},
|
||||
overall: SERVICE_STATUS.CHECKING
|
||||
};
|
||||
|
||||
// Determine overall status
|
||||
const availableServices = [
|
||||
newStatus.apiGateway.available,
|
||||
newStatus.serviceAdapters.available,
|
||||
newStatus.apiDocs.available
|
||||
].filter(Boolean).length;
|
||||
|
||||
newStatus.overall = determineServiceStatus(availableServices, 3);
|
||||
|
||||
setStatus(newStatus);
|
||||
};
|
||||
|
||||
checkServices();
|
||||
|
||||
// Check services every 30 seconds
|
||||
const interval = setInterval(checkServices, API_CONFIG.REFRESH_INTERVALS.SERVICE_STATUS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
export const useSystemData = () => {
|
||||
const [data, setData] = useState({
|
||||
loading: true,
|
||||
systemStats: fallbackData.systemStats,
|
||||
services: fallbackData.services,
|
||||
events: fallbackData.events,
|
||||
error: null
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setData(prev => ({ ...prev, loading: true }));
|
||||
|
||||
try {
|
||||
// Try to fetch real data from services
|
||||
const [metricsResult, servicesResult, eventsResult] = await Promise.allSettled([
|
||||
apiGateway.getSystemMetrics(),
|
||||
serviceAdapters.getServices(),
|
||||
serviceAdapters.getEvents(10)
|
||||
]);
|
||||
|
||||
const systemStats = metricsResult.status === 'fulfilled' && metricsResult.value.success
|
||||
? metricsResult.value.data
|
||||
: fallbackData.systemStats;
|
||||
|
||||
const services = servicesResult.status === 'fulfilled' && servicesResult.value.success
|
||||
? formatServiceData(servicesResult.value.data)
|
||||
: fallbackData.services;
|
||||
|
||||
const events = eventsResult.status === 'fulfilled' && eventsResult.value.success
|
||||
? formatEventData(eventsResult.value.data.events)
|
||||
: fallbackData.events;
|
||||
|
||||
setData({
|
||||
loading: false,
|
||||
systemStats,
|
||||
services,
|
||||
events,
|
||||
error: null
|
||||
});
|
||||
} catch (error) {
|
||||
setData({
|
||||
loading: false,
|
||||
systemStats: fallbackData.systemStats,
|
||||
services: fallbackData.services,
|
||||
events: fallbackData.events,
|
||||
error: 'Failed to fetch data from services'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
// Refresh data every 60 seconds
|
||||
const interval = setInterval(fetchData, API_CONFIG.REFRESH_INTERVALS.SYSTEM_DATA);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return data;
|
||||
};
|
||||
193
frontend/src/services/api.js
Normal file
193
frontend/src/services/api.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import axios from 'axios';
|
||||
import { API_CONFIG, SERVICE_URLS, FALLBACK_DATA } from '../constants';
|
||||
import { handleRequestError, formatServiceData, formatEventData } from '../utils/errorHandling';
|
||||
|
||||
// Create axios instances with timeout and error handling
|
||||
const apiClient = axios.create({
|
||||
baseURL: SERVICE_URLS.API_GATEWAY,
|
||||
timeout: API_CONFIG.TIMEOUT,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const adaptersClient = axios.create({
|
||||
baseURL: SERVICE_URLS.SERVICE_ADAPTERS,
|
||||
timeout: API_CONFIG.TIMEOUT,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const docsClient = axios.create({
|
||||
baseURL: SERVICE_URLS.API_DOCS,
|
||||
timeout: API_CONFIG.TIMEOUT,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// API Gateway endpoints
|
||||
export const apiGateway = {
|
||||
// Health check
|
||||
health: async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/health');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
// Dashboards
|
||||
getDashboards: async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/dashboards');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: [], ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
// System data
|
||||
getEvents: async (params = {}) => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/system/events', { params });
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: [], ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
getDeviceStates: async (params = {}) => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/system/device-states', { params });
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: [], ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
getSystemMetrics: async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/system/metrics');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: null, ...handleRequestError(error) };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Service Adapters endpoints
|
||||
export const serviceAdapters = {
|
||||
// Health check
|
||||
health: async () => {
|
||||
try {
|
||||
const response = await adaptersClient.get('/health');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
// Services status
|
||||
getServices: async () => {
|
||||
try {
|
||||
const response = await adaptersClient.get('/services');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: {}, ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
// Home Assistant
|
||||
getHAEntities: async () => {
|
||||
try {
|
||||
const response = await adaptersClient.get('/home-assistant/entities');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: { entities: [] }, ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
// Frigate
|
||||
getFrigateEvents: async () => {
|
||||
try {
|
||||
const response = await adaptersClient.get('/frigate/events');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: { events: [] }, ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
getFrigateCameras: async () => {
|
||||
try {
|
||||
const response = await adaptersClient.get('/frigate/cameras');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: { cameras: [] }, ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
// Immich
|
||||
getImmichAssets: async () => {
|
||||
try {
|
||||
const response = await adaptersClient.get('/immich/assets');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: { assets: [] }, ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
getImmichAlbums: async () => {
|
||||
try {
|
||||
const response = await adaptersClient.get('/immich/albums');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: { albums: [] }, ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
// Events
|
||||
getEvents: async (limit = 100) => {
|
||||
try {
|
||||
const response = await adaptersClient.get('/events', { params: { limit } });
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: { events: [] }, ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
publishEvent: async (eventData) => {
|
||||
try {
|
||||
const response = await adaptersClient.post('/publish-event', eventData);
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, ...handleRequestError(error) };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// API Docs endpoints
|
||||
export const apiDocs = {
|
||||
health: async () => {
|
||||
try {
|
||||
const response = await docsClient.get('/health');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, ...handleRequestError(error) };
|
||||
}
|
||||
},
|
||||
|
||||
getServices: async () => {
|
||||
try {
|
||||
const response = await docsClient.get('/services');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, data: {}, ...handleRequestError(error) };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Export fallback data from constants
|
||||
export const fallbackData = FALLBACK_DATA;
|
||||
65
frontend/src/utils/errorHandling.js
Normal file
65
frontend/src/utils/errorHandling.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ERROR_MESSAGES } from '../constants';
|
||||
|
||||
/**
|
||||
* Handles API request errors and returns user-friendly messages
|
||||
* @param {Error} error - The error object from axios
|
||||
* @returns {Object} - Error object with user-friendly message
|
||||
*/
|
||||
export const handleRequestError = (error) => {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return { error: ERROR_MESSAGES.CONNECTION_TIMEOUT };
|
||||
}
|
||||
if (error.response) {
|
||||
return { error: `${ERROR_MESSAGES.SERVICE_ERROR}: ${error.response.status}` };
|
||||
}
|
||||
if (error.request) {
|
||||
return { error: ERROR_MESSAGES.SERVICE_UNAVAILABLE };
|
||||
}
|
||||
return { error: ERROR_MESSAGES.UNKNOWN_ERROR };
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines service status based on availability count
|
||||
* @param {number} availableCount - Number of available services
|
||||
* @param {number} totalCount - Total number of services
|
||||
* @returns {string} - Service status
|
||||
*/
|
||||
export const determineServiceStatus = (availableCount, totalCount) => {
|
||||
if (availableCount === 0) return 'offline';
|
||||
if (availableCount === totalCount) return 'online';
|
||||
return 'partial';
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats service data for display
|
||||
* @param {Object} serviceData - Raw service data
|
||||
* @returns {Array} - Formatted service array
|
||||
*/
|
||||
export const formatServiceData = (serviceData) => {
|
||||
if (!serviceData || typeof serviceData !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(serviceData).map(([key, service]) => ({
|
||||
name: service.name || key,
|
||||
status: service.status === 'healthy' ? 'online' : 'offline',
|
||||
uptime: service.responseTime || '0d 0h'
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats event data for display
|
||||
* @param {Array} events - Raw event data
|
||||
* @returns {Array} - Formatted event array
|
||||
*/
|
||||
export const formatEventData = (events) => {
|
||||
if (!Array.isArray(events)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return events.map(event => ({
|
||||
time: new Date(event.timestamp).toLocaleString(),
|
||||
event: `${event.event_type} from ${event.service}`,
|
||||
service: event.service
|
||||
}));
|
||||
};
|
||||
Reference in New Issue
Block a user