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>
89 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
}
|