Christian Mueller a50ef96eb7 feat(frontend): Task 15 - all React pages and components
Implements the full SPA UI:
- App.tsx with BrowserRouter, ProtectedRoute, and all page routes
- Sidebar with NavLink active state and logout
- Login page with JWT auth flow
- Dashboard with parallel data fetching and status tiles
- MqttBrokers: CRUD with test-connection button and Topics link
- MqttTopics: discovery, filter, json_path inline edit, ignore
- PgSources: CRUD form with all required fields
- Datapoints: table with inline label/unit editing via onBlur
- Displays: layout dropdown, refresh interval, last-seen timestamp
- Layouts: left panel list + right panel element editor
- EpaperPreview: scaled e-paper simulation with live data support
- StatusBadge: color-coded status chip

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:06:40 +02:00

89 lines
3.7 KiB
TypeScript

import { useEffect, useState } from 'react';
import { api } from '../api/client';
import StatusBadge from '../components/StatusBadge';
interface Broker { id: number; name: string; status: string }
interface Datapoint { id: number; name: string; label: string; value: string | null; unit: string | null; source_type: string; status: string; updated_at: string | null }
interface Display { id: number; name: string; last_seen: string | null }
export default function Dashboard() {
const [brokers, setBrokers] = useState<Broker[]>([]);
const [datapoints, setDatapoints] = useState<Datapoint[]>([]);
const [displays, setDisplays] = useState<Display[]>([]);
const [error, setError] = useState('');
useEffect(() => {
Promise.all([
api.get<Broker[]>('/api/mqtt/brokers'),
api.get<Datapoint[]>('/api/datapoints'),
api.get<Display[]>('/api/displays'),
])
.then(([b, d, disp]) => {
setBrokers(b);
setDatapoints(d);
setDisplays(disp);
})
.catch((err) => setError((err as Error).message));
}, []);
const lastUpdate = datapoints
.map((d) => d.updated_at)
.filter(Boolean)
.sort()
.at(-1);
const tiles = [
{ label: 'MQTT Broker', value: brokers.length, color: 'bg-blue-50 border-blue-200' },
{ label: 'Datenpunkte', value: datapoints.length, color: 'bg-green-50 border-green-200' },
{ label: 'Displays', value: displays.length, color: 'bg-purple-50 border-purple-200' },
{ label: 'Letztes Update', value: lastUpdate ? new Date(lastUpdate).toLocaleString('de') : '-', color: 'bg-yellow-50 border-yellow-200' },
];
return (
<div>
<h2 className="text-xl font-semibold text-gray-800 mb-6">Dashboard</h2>
{error && <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm">{error}</div>}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{tiles.map(({ label, value, color }) => (
<div key={label} className={`rounded-lg border p-4 ${color}`}>
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">{label}</p>
<p className="text-2xl font-bold text-gray-800">{value}</p>
</div>
))}
</div>
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="px-4 py-3 border-b border-gray-200">
<h3 className="font-medium text-gray-700">Datenpunkte</h3>
</div>
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Bezeichnung</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Wert</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Einheit</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{datapoints.length === 0 && (
<tr>
<td colSpan={4} className="px-4 py-6 text-center text-gray-400">Keine Datenpunkte vorhanden</td>
</tr>
)}
{datapoints.map((dp) => (
<tr key={dp.id} className="hover:bg-gray-50">
<td className="px-4 py-2 text-gray-700">{dp.label || dp.name}</td>
<td className="px-4 py-2 font-semibold text-gray-900">{dp.value ?? '-'}</td>
<td className="px-4 py-2 text-gray-500">{dp.unit ?? ''}</td>
<td className="px-4 py-2"><StatusBadge status={dp.status} /></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}