Initial commit - copied workspace after database cleanup
This commit is contained in:
204
dashboard/src/dashboard.tsx
Normal file
204
dashboard/src/dashboard.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { fetchGroupsWithClients, restartClient } from './apiClients';
|
||||
import type { Group, Client } from './apiClients';
|
||||
import {
|
||||
GridComponent,
|
||||
ColumnsDirective,
|
||||
ColumnDirective,
|
||||
Page,
|
||||
DetailRow,
|
||||
Inject,
|
||||
Sort,
|
||||
} from '@syncfusion/ej2-react-grids';
|
||||
|
||||
const REFRESH_INTERVAL = 15000; // 15 Sekunden
|
||||
|
||||
// Typ für Collapse-Event
|
||||
// type DetailRowCollapseArgs = {
|
||||
// data?: { id?: string | number };
|
||||
// };
|
||||
|
||||
// Typ für DataBound-Event
|
||||
type DetailRowDataBoundArgs = {
|
||||
data?: { id?: string | number };
|
||||
};
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [expandedGroupIds, setExpandedGroupIds] = useState<string[]>([]);
|
||||
const gridRef = useRef<GridComponent | null>(null);
|
||||
|
||||
// Funktion für das Schließen einer Gruppe (Collapse)
|
||||
// const onDetailCollapse = (args: DetailRowCollapseArgs) => {
|
||||
// if (args && args.data && args.data.id) {
|
||||
// const groupId = String(args.data.id);
|
||||
// setExpandedGroupIds(prev => prev.filter(id => String(id) !== groupId));
|
||||
// }
|
||||
// };
|
||||
|
||||
// // Registriere das Event nach dem Mount am Grid
|
||||
// useEffect(() => {
|
||||
// if (gridRef.current) {
|
||||
// gridRef.current.detailCollapse = onDetailCollapse;
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
// Optimiertes Update: Nur bei echten Datenänderungen wird das Grid aktualisiert
|
||||
useEffect(() => {
|
||||
let lastGroups: Group[] = [];
|
||||
const fetchAndUpdate = async () => {
|
||||
const newGroups = await fetchGroupsWithClients();
|
||||
// Vergleiche nur die relevanten Felder (id, clients, is_alive)
|
||||
const changed =
|
||||
lastGroups.length !== newGroups.length ||
|
||||
lastGroups.some((g, i) => {
|
||||
const ng = newGroups[i];
|
||||
if (!ng || g.id !== ng.id || g.clients.length !== ng.clients.length) return true;
|
||||
// Optional: Vergleiche tiefer, z.B. Alive-Status
|
||||
for (let j = 0; j < g.clients.length; j++) {
|
||||
if (
|
||||
g.clients[j].uuid !== ng.clients[j].uuid ||
|
||||
g.clients[j].is_alive !== ng.clients[j].is_alive
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (changed) {
|
||||
setGroups(newGroups);
|
||||
lastGroups = newGroups;
|
||||
setTimeout(() => {
|
||||
expandedGroupIds.forEach(id => {
|
||||
const rowIndex = newGroups.findIndex(g => String(g.id) === String(id));
|
||||
if (rowIndex !== -1 && gridRef.current) {
|
||||
gridRef.current.detailRowModule.expand(rowIndex);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAndUpdate();
|
||||
const interval = setInterval(fetchAndUpdate, REFRESH_INTERVAL);
|
||||
return () => clearInterval(interval);
|
||||
}, [expandedGroupIds]);
|
||||
|
||||
// Health-Badge
|
||||
const getHealthBadge = (group: Group) => {
|
||||
const total = group.clients.length;
|
||||
const alive = group.clients.filter((c: Client) => c.is_alive).length;
|
||||
const ratio = total === 0 ? 0 : alive / total;
|
||||
let color = 'danger';
|
||||
let text = `${alive} / ${total} offline`;
|
||||
if (ratio === 1) {
|
||||
color = 'success';
|
||||
text = `${alive} / ${total} alive`;
|
||||
} else if (ratio >= 0.5) {
|
||||
color = 'warning';
|
||||
text = `${alive} / ${total} teilw. alive`;
|
||||
}
|
||||
return <span className={`e-badge e-badge-${color}`}>{text}</span>;
|
||||
};
|
||||
|
||||
// Einfache Tabelle für Clients einer Gruppe
|
||||
const getClientTable = (group: Group) => (
|
||||
<div style={{ maxHeight: 300, overflowY: 'auto', marginBottom: '5px' }}>
|
||||
<GridComponent dataSource={group.clients} allowSorting={true} height={'auto'}>
|
||||
<ColumnsDirective>
|
||||
<ColumnDirective field="description" headerText="Beschreibung" width="150" />
|
||||
<ColumnDirective field="ip" headerText="IP" width="120" />
|
||||
{/* <ColumnDirective
|
||||
field="last_alive"
|
||||
headerText="Letztes Lebenszeichen"
|
||||
width="180"
|
||||
template={(props: { last_alive: string | null }) => {
|
||||
if (!props.last_alive) return '-';
|
||||
const dateStr = props.last_alive.endsWith('Z')
|
||||
? props.last_alive
|
||||
: props.last_alive + 'Z';
|
||||
const date = new Date(dateStr);
|
||||
return isNaN(date.getTime()) ? props.last_alive : date.toLocaleString();
|
||||
}}
|
||||
/> */}
|
||||
<ColumnDirective
|
||||
field="is_alive"
|
||||
headerText="Alive"
|
||||
width="100"
|
||||
template={(props: { is_alive: boolean }) => (
|
||||
<span className={`e-badge e-badge-${props.is_alive ? 'success' : 'danger'}`}>
|
||||
{props.is_alive ? 'alive' : 'offline'}
|
||||
</span>
|
||||
)}
|
||||
sortComparer={(a, b) => (a === b ? 0 : a ? -1 : 1)}
|
||||
/>
|
||||
<ColumnDirective
|
||||
headerText="Aktionen"
|
||||
width="150"
|
||||
template={(props: { uuid: string }) => (
|
||||
<button className="e-btn e-primary" onClick={() => handleRestartClient(props.uuid)}>
|
||||
Neustart
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
</ColumnsDirective>
|
||||
<Inject services={[Sort]} />
|
||||
</GridComponent>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Neustart-Logik
|
||||
const handleRestartClient = async (uuid: string) => {
|
||||
try {
|
||||
const result = await restartClient(uuid);
|
||||
alert(`Neustart erfolgreich: ${result.message}`);
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === 'object' && 'message' in error) {
|
||||
alert(`Fehler beim Neustart: ${(error as { message: string }).message}`);
|
||||
} else {
|
||||
alert('Unbekannter Fehler beim Neustart');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// SyncFusion Grid liefert im Event die Zeile/Gruppe
|
||||
const onDetailDataBound = (args: DetailRowDataBoundArgs) => {
|
||||
if (args && args.data && args.data.id) {
|
||||
const groupId = String(args.data.id);
|
||||
setExpandedGroupIds(prev => (prev.includes(groupId) ? prev : [...prev, groupId]));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header className="mb-8 pb-4 border-b-2 border-[#d6c3a6]">
|
||||
<h2 className="text-3xl font-extrabold mb-2">Dashboard</h2>
|
||||
</header>
|
||||
<h3 className="text-lg font-semibold mt-6 mb-4">Raumgruppen Übersicht</h3>
|
||||
<GridComponent
|
||||
dataSource={groups}
|
||||
allowPaging={true}
|
||||
pageSettings={{ pageSize: 5 }}
|
||||
height={400}
|
||||
detailTemplate={(props: Group) => getClientTable(props)}
|
||||
detailDataBound={onDetailDataBound}
|
||||
ref={gridRef}
|
||||
>
|
||||
<Inject services={[Page, DetailRow]} />
|
||||
<ColumnsDirective>
|
||||
<ColumnDirective field="name" headerText="Raumgruppe" width="180" />
|
||||
<ColumnDirective
|
||||
headerText="Health"
|
||||
width="160"
|
||||
template={(props: Group) => getHealthBadge(props)}
|
||||
/>
|
||||
</ColumnsDirective>
|
||||
</GridComponent>
|
||||
{groups.length === 0 && (
|
||||
<div className="col-span-full text-center text-gray-400">Keine Gruppen gefunden.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
Reference in New Issue
Block a user