test communication scheduler<->simclient
This commit is contained in:
20
dashboard/package-lock.json
generated
20
dashboard/package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@syncfusion/ej2-react-grids": "^30.1.37",
|
||||
"@syncfusion/ej2-react-inputs": "^30.1.38",
|
||||
"@syncfusion/ej2-react-kanban": "^30.1.37",
|
||||
"@syncfusion/ej2-react-layouts": "^30.1.40",
|
||||
"@syncfusion/ej2-react-notifications": "^30.1.37",
|
||||
"@syncfusion/ej2-react-popups": "^30.1.37",
|
||||
"@syncfusion/ej2-react-schedule": "^30.1.37",
|
||||
@@ -1165,12 +1166,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@syncfusion/ej2-layouts": {
|
||||
"version": "30.1.37",
|
||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-layouts/-/ej2-layouts-30.1.37.tgz",
|
||||
"integrity": "sha512-EPI00OSBMuzxp8od6jTOK2LRlv5IWtN4WmpIklUe34vev0qG9HLo8yT2BKCUIo5TRm4xEUXx0mQM7ZSKw2iHig==",
|
||||
"version": "30.1.40",
|
||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-layouts/-/ej2-layouts-30.1.40.tgz",
|
||||
"integrity": "sha512-PPv+brJOOkaMp+HZ7IDq7Tc8aYMYrpP7i0cp/b2W8fFTwcXoI06l4oRp65NAy1THnU82m2pRnbTETyrSDMu+TA==",
|
||||
"license": "SEE LICENSE IN license",
|
||||
"dependencies": {
|
||||
"@syncfusion/ej2-base": "~30.1.37"
|
||||
"@syncfusion/ej2-base": "~30.1.38"
|
||||
}
|
||||
},
|
||||
"node_modules/@syncfusion/ej2-lists": {
|
||||
@@ -1315,6 +1316,17 @@
|
||||
"@syncfusion/ej2-react-base": "~30.1.37"
|
||||
}
|
||||
},
|
||||
"node_modules/@syncfusion/ej2-react-layouts": {
|
||||
"version": "30.1.40",
|
||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-layouts/-/ej2-react-layouts-30.1.40.tgz",
|
||||
"integrity": "sha512-gOIo3DOEy+m0QF8vuj+dEB/PdnEnv4n+8pvCIPfiCcatY46rPSv4Lzh8q4PixGfFwauLfljAIdm+3gquxvfJIA==",
|
||||
"license": "SEE LICENSE IN license",
|
||||
"dependencies": {
|
||||
"@syncfusion/ej2-base": "~30.1.38",
|
||||
"@syncfusion/ej2-layouts": "30.1.40",
|
||||
"@syncfusion/ej2-react-base": "~30.1.37"
|
||||
}
|
||||
},
|
||||
"node_modules/@syncfusion/ej2-react-notifications": {
|
||||
"version": "30.1.37",
|
||||
"resolved": "https://registry.npmjs.org/@syncfusion/ej2-react-notifications/-/ej2-react-notifications-30.1.37.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@syncfusion/ej2-react-grids": "^30.1.37",
|
||||
"@syncfusion/ej2-react-inputs": "^30.1.38",
|
||||
"@syncfusion/ej2-react-kanban": "^30.1.37",
|
||||
"@syncfusion/ej2-react-layouts": "^30.1.40",
|
||||
"@syncfusion/ej2-react-notifications": "^30.1.37",
|
||||
"@syncfusion/ej2-react-popups": "^30.1.37",
|
||||
"@syncfusion/ej2-react-schedule": "^30.1.37",
|
||||
|
||||
@@ -32,7 +32,6 @@ const sidebarItems = [
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { fetchClientsWithoutDescription } from './apiClients';
|
||||
|
||||
// ENV aus .env holen (Platzhalter, im echten Projekt über process.env oder API)
|
||||
const ENV = import.meta.env.VITE_ENV || 'development';
|
||||
@@ -128,7 +127,6 @@ const Layout: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function useLoginCheck() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
|
||||
@@ -150,17 +148,7 @@ const App: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const isLoggedIn = useLoginCheck();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) return;
|
||||
fetchClientsWithoutDescription().then(list => {
|
||||
if (list.length > 0) {
|
||||
console.log('[Navigation] Weiterleitung zu /clients wegen fehlender Beschreibung');
|
||||
navigate('/clients');
|
||||
} else {
|
||||
console.log('[Navigation] Dashboard wird angezeigt, alle Clients haben Beschreibung');
|
||||
}
|
||||
});
|
||||
}, [isLoggedIn, navigate]);
|
||||
// Automatische Navigation zu /clients bei leerer Beschreibung entfernt
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export interface Client {
|
||||
uuid: string;
|
||||
hardware_token?: string;
|
||||
@@ -51,3 +50,13 @@ export async function updateClientGroup(clientIds: string[], groupName: string)
|
||||
if (!res.ok) throw new Error('Fehler beim Aktualisieren der Clients');
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function updateClient(uuid: string, data: { description?: string; model?: string }) {
|
||||
const res = await fetch(`/api/clients/${uuid}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) throw new Error('Fehler beim Aktualisieren des Clients');
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
@@ -1,17 +1,108 @@
|
||||
import SetupModeButton from './components/SetupModeButton';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { fetchClients, fetchClientsWithoutDescription, setClientDescription } from './apiClients';
|
||||
// ...ButtonComponent entfernt...
|
||||
// Card-Komponente wird als eigenes Layout umgesetzt
|
||||
import { fetchClients, updateClient } from './apiClients';
|
||||
import type { Client } from './apiClients';
|
||||
import {
|
||||
GridComponent,
|
||||
ColumnsDirective,
|
||||
ColumnDirective,
|
||||
Page,
|
||||
Inject,
|
||||
Toolbar,
|
||||
Search,
|
||||
Sort,
|
||||
Edit,
|
||||
} from '@syncfusion/ej2-react-grids';
|
||||
|
||||
// Dummy Modalbox (ersetzbar durch SyncFusion Dialog)
|
||||
function ModalBox({ open, onClose }) {
|
||||
if (!open) return null;
|
||||
// Raumgruppen werden dynamisch aus der API geladen
|
||||
|
||||
interface DetailsModalProps {
|
||||
open: boolean;
|
||||
client: Client | null;
|
||||
groupIdToName: Record<string | number, string>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function DetailsModal({ open, client, groupIdToName, onClose }: DetailsModalProps) {
|
||||
if (!open || !client) return null;
|
||||
return (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.3)', zIndex: 1000 }}>
|
||||
<div style={{ background: 'white', padding: 32, margin: '100px auto', maxWidth: 400, borderRadius: 8 }}>
|
||||
<h3>Neue Clients ohne Beschreibung!</h3>
|
||||
<p>Bitte ergänzen Sie die Beschreibung für neue Clients.</p>
|
||||
<button onClick={onClose}>Speichern (Dummy)</button>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'white',
|
||||
padding: 0,
|
||||
margin: '100px auto',
|
||||
maxWidth: 500,
|
||||
borderRadius: 12,
|
||||
boxShadow: '0 4px 24px rgba(0,0,0,0.12)',
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: 32 }}>
|
||||
<h3 style={{ fontSize: '1.25rem', fontWeight: 700, marginBottom: 18 }}>Client-Details</h3>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: 24 }}>
|
||||
<tbody>
|
||||
{Object.entries(client)
|
||||
.filter(
|
||||
([key]) =>
|
||||
![
|
||||
'index',
|
||||
'is_active',
|
||||
'type',
|
||||
'column',
|
||||
'group_name',
|
||||
'foreignKeyData',
|
||||
'hardware_token',
|
||||
].includes(key)
|
||||
)
|
||||
.map(([key, value]) => (
|
||||
<tr key={key}>
|
||||
<td style={{ fontWeight: 'bold', padding: '6px 8px' }}>
|
||||
{key === 'group_id'
|
||||
? 'Raumgruppe'
|
||||
: key === 'ip'
|
||||
? 'IP-Adresse'
|
||||
: key === 'registration_time'
|
||||
? 'Registriert am'
|
||||
: key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
:
|
||||
</td>
|
||||
<td style={{ padding: '6px 8px' }}>
|
||||
{key === 'group_id'
|
||||
? value !== undefined
|
||||
? groupIdToName[value as string | number] || value
|
||||
: ''
|
||||
: key === 'registration_time' && value
|
||||
? new Date(
|
||||
(value as string).endsWith('Z') ? (value as string) : value + 'Z'
|
||||
).toLocaleString()
|
||||
: key === 'last_alive' && value
|
||||
? new Date(
|
||||
(value as string).endsWith('Z') ? (value as string) : value + 'Z'
|
||||
).toLocaleString()
|
||||
: String(value)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<button className="e-btn e-outline" onClick={onClose}>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -19,42 +110,122 @@ function ModalBox({ open, onClose }) {
|
||||
|
||||
const Clients: React.FC = () => {
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [groups, setGroups] = useState<{ id: number; name: string }[]>([]);
|
||||
const [selectedClient, setSelectedClient] = useState<Client | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchClients().then(setClients);
|
||||
fetchClientsWithoutDescription().then(list => {
|
||||
if (list.length > 0) setShowModal(true);
|
||||
});
|
||||
// Gruppen auslesen
|
||||
import('./apiGroups').then(mod => mod.fetchGroups()).then(setGroups);
|
||||
}, []);
|
||||
|
||||
// Map group_id zu group_name
|
||||
const groupIdToName: Record<string | number, string> = {};
|
||||
groups.forEach(g => {
|
||||
groupIdToName[g.id] = g.name;
|
||||
});
|
||||
|
||||
// DataGrid data mit korrektem Gruppennamen und formatierten Zeitangaben
|
||||
const gridData = clients.map(c => ({
|
||||
...c,
|
||||
group_name: c.group_id !== undefined ? groupIdToName[c.group_id] || String(c.group_id) : '',
|
||||
last_alive: c.last_alive
|
||||
? new Date(
|
||||
(c.last_alive as string).endsWith('Z') ? (c.last_alive as string) : c.last_alive + 'Z'
|
||||
).toLocaleString()
|
||||
: '',
|
||||
}));
|
||||
|
||||
// DataGrid row template für Details-Button
|
||||
const detailsButtonTemplate = (props: Client) => (
|
||||
<button
|
||||
className="e-btn e-primary"
|
||||
onClick={() => setSelectedClient(props)}
|
||||
style={{ minWidth: 80 }}
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-4">Client-Übersicht</h2>
|
||||
<table className="min-w-full border">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>UUID</th>
|
||||
<th>Hostname</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Gruppe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{clients.map(c => (
|
||||
<tr key={c.uuid}>
|
||||
<td>{c.uuid}</td>
|
||||
<td>{c.hostname}</td>
|
||||
<td>{c.description}</td>
|
||||
<td>{c.group_id}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-bold">Client-Übersicht</h2>
|
||||
<SetupModeButton />
|
||||
</div>
|
||||
<ModalBox open={showModal} onClose={() => setShowModal(false)} />
|
||||
{groups.length > 0 ? (
|
||||
<>
|
||||
<GridComponent
|
||||
dataSource={gridData}
|
||||
allowPaging={true}
|
||||
pageSettings={{ pageSize: 10 }}
|
||||
toolbar={['Search', 'Edit', 'Update', 'Cancel']}
|
||||
allowSorting={true}
|
||||
allowFiltering={true}
|
||||
height={400}
|
||||
editSettings={{
|
||||
allowEditing: true,
|
||||
allowAdding: false,
|
||||
allowDeleting: false,
|
||||
mode: 'Normal',
|
||||
}}
|
||||
actionComplete={async (args: { requestType: string; data: any }) => {
|
||||
if (args.requestType === 'save') {
|
||||
const { uuid, description, model } = args.data;
|
||||
// API-Aufruf zum Speichern
|
||||
await updateClient(uuid, { description, model });
|
||||
// Nach dem Speichern neu laden
|
||||
fetchClients().then(setClients);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ColumnsDirective>
|
||||
<ColumnDirective
|
||||
field="description"
|
||||
headerText="Beschreibung"
|
||||
width="150"
|
||||
allowEditing={true}
|
||||
/>
|
||||
<ColumnDirective
|
||||
field="group_name"
|
||||
headerText="Raumgruppe"
|
||||
width="120"
|
||||
allowEditing={false}
|
||||
/>
|
||||
<ColumnDirective field="uuid" headerText="UUID" width="220" allowEditing={false} />
|
||||
<ColumnDirective
|
||||
field="ip"
|
||||
headerText="IP-Adresse"
|
||||
width="150"
|
||||
allowEditing={false}
|
||||
/>
|
||||
<ColumnDirective
|
||||
field="last_alive"
|
||||
headerText="Last Alive"
|
||||
width="120"
|
||||
allowEditing={false}
|
||||
/>
|
||||
<ColumnDirective field="model" headerText="Model" width="120" allowEditing={true} />
|
||||
<ColumnDirective
|
||||
headerText="Details"
|
||||
width="100"
|
||||
template={detailsButtonTemplate}
|
||||
textAlign="Center"
|
||||
allowEditing={false}
|
||||
/>
|
||||
</ColumnsDirective>
|
||||
<Inject services={[Page, Toolbar, Search, Sort, Edit]} />
|
||||
</GridComponent>
|
||||
<DetailsModal
|
||||
open={!!selectedClient}
|
||||
client={selectedClient}
|
||||
groupIdToName={groupIdToName}
|
||||
onClose={() => setSelectedClient(null)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-gray-500">Raumgruppen werden geladen ...</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user