This commit is contained in:
jhartworks
2026-06-25 10:20:38 +02:00
parent f0cb94ce4e
commit bf1214239f
5 changed files with 680 additions and 109 deletions
+85 -52
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { api } from "./api.js";
import Gauge from "./components/Gauge.jsx";
@@ -13,8 +13,10 @@ const views = [
const adminSections = [
{ id: "aliases", label: "Aliase" },
{ id: "users", label: "Benutzer" },
{ id: "sources", label: "Quellen" },
{ id: "users", label: "Benutzer" },
{ id: "roles", label: "Rollen" },
{ id: "debug", label: "Debug" },
];
const emptyTrendRange = {
@@ -95,6 +97,32 @@ function getDefaultProtocolPort(protocol) {
return PROTOCOL_DEFAULT_PORTS[protocol] || "";
}
function getDefaultPointDataType(protocol) {
if (protocol === "knx-ip") {
return "group-address";
}
if (protocol === "opc-ua") {
return "node-id";
}
if (protocol === "bacnet-ip") {
return "analogInput";
}
return "holding-register";
}
function getManualAddressLabel(protocol) {
if (protocol === "knx-ip") {
return "Gruppenadresse";
}
if (protocol === "opc-ua") {
return "NodeId";
}
if (protocol === "bacnet-ip") {
return "Objekt-Instanz / Adresse";
}
return "Adresse / Register";
}
function formatValue(value) {
return Number.isFinite(Number(value)) ? Number(value).toFixed(2) : "--";
}
@@ -488,16 +516,21 @@ export default function App() {
const [selectedSourceId, setSelectedSourceId] = useState("");
const [isCreatingSource, setIsCreatingSource] = useState(false);
const [sourceDraft, setSourceDraft] = useState(defaultSourceDraft);
const [manualDatapointDraft, setManualDatapointDraft] = useState({ alias: "", address: "", pointIndex: "", unit: "°C", dataType: "holding-register", kind: "analog" });
const [manualDatapointDraft, setManualDatapointDraft] = useState({ alias: "", address: "", pointIndex: "", unit: "°C", dataType: getDefaultPointDataType(defaultSourceDraft.protocol), kind: "analog" });
const [scanResults, setScanResults] = useState({ opcua: null, bacnet: null });
const [debugSnapshot, setDebugSnapshot] = useState(null);
const trendIsps = useMemo(() => isps.filter((isp) => Number(isp.rowCount) > 0), [isps]);
const selectedMetadata = selectedIsp ? metadataCache[selectedIsp] : null;
const canViewDashboard = hasPermission(user, "view_dashboard");
const canViewTrends = hasPermission(user, "view_trends");
const canEditAliases = hasPermission(user, "edit_aliases");
const canManageSources = hasPermission(user, "manage_sources");
const canManageUsers = hasPermission(user, "manage_users");
const canManageRoles = hasPermission(user, "manage_roles");
const canViewDebug = hasPermission(user, "view_debug");
const canAccessAdmin = canEditAliases || canManageSources || canManageUsers || canManageRoles || canViewDebug;
const firstAdminSection = canEditAliases ? "aliases" : canManageSources ? "sources" : canManageUsers ? "users" : canManageRoles ? "roles" : canViewDebug ? "debug" : "aliases";
const sidebarCollapsed = !sidebarPinned && !sidebarHover;
const currentView = views.find((view) => view.id === activeView) || views[0];
const selectionListClassName = selections.length <= 2 ? "selection-list compact" : "selection-list";
@@ -529,6 +562,10 @@ export default function App() {
return () => media.removeEventListener("change", applyTheme);
}, [themeMode]);
useEffect(() => {
resetFlash();
}, [activeView, adminSection]);
useEffect(() => {
if (!token) {
setSessionReady(true);
@@ -748,25 +785,50 @@ export default function App() {
});
setManualDatapointDraft((current) => ({
...current,
dataType: currentSource.protocol === "knx-ip" ? "group-address" : "holding-register",
dataType: getDefaultPointDataType(currentSource.protocol || "modbus-tcp"),
}));
}, [currentSource]);
useEffect(() => {
if (!canViewDashboard && activeView === "dashboard") {
setActiveView(canViewTrends ? "trends" : canAccessAdmin ? "admin" : "settings");
return;
}
if (!canViewTrends && activeView === "trends") {
setActiveView(canViewDashboard ? "dashboard" : canAccessAdmin ? "admin" : "settings");
return;
}
if (!canAccessAdmin && activeView === "admin") {
setActiveView(canViewDashboard ? "dashboard" : canViewTrends ? "trends" : "settings");
return;
}
if (!canEditAliases && adminSection === "aliases") {
setAdminSection(firstAdminSection);
return;
}
if (!(canManageSources || canEditAliases) && adminSection === "sources") {
setAdminSection(firstAdminSection);
return;
}
if (!canManageUsers && adminSection === "users") {
setAdminSection("aliases");
setAdminSection(firstAdminSection);
return;
}
if (!canManageRoles && adminSection === "roles") {
setAdminSection("aliases");
setAdminSection(firstAdminSection);
return;
}
if (!canViewDebug && adminSection === "debug") {
setAdminSection("aliases");
setAdminSection(firstAdminSection);
}
}, [activeView, adminSection, canAccessAdmin, canManageRoles, canManageUsers, canViewDashboard, canViewDebug, canViewTrends]);
}, [activeView, adminSection, canAccessAdmin, canEditAliases, canManageRoles, canManageSources, canManageUsers, canViewDashboard, canViewDebug, canViewTrends, firstAdminSection]);
useEffect(() => {
if (!selectedSelectionId) {
@@ -1108,7 +1170,7 @@ export default function App() {
const startNewSource = () => {
setIsCreatingSource(true);
setSelectedSourceId("");
setManualDatapointDraft({ alias: "", address: "", pointIndex: "", unit: "°C", dataType: "holding-register", kind: "analog" });
setManualDatapointDraft({ alias: "", address: "", pointIndex: "", unit: "°C", dataType: getDefaultPointDataType(defaultSourceDraft.protocol), kind: "analog" });
setSourceDraft({
...defaultSourceDraft,
port: getDefaultProtocolPort(defaultSourceDraft.protocol),
@@ -1128,6 +1190,10 @@ export default function App() {
tableName: current.id ? current.tableName : buildCollectorTableName(current.name, nextProtocol),
};
});
setManualDatapointDraft((current) => ({
...current,
dataType: getDefaultPointDataType(nextProtocol),
}));
};
const saveCollectorSource = async () => {
@@ -1212,11 +1278,12 @@ export default function App() {
pointIndex: manualDatapointDraft.pointIndex,
unit: manualDatapointDraft.unit || "°C",
dataType: manualDatapointDraft.dataType,
nodeId: sourceDraft.protocol === "opc-ua" ? manualDatapointDraft.address : "",
kind: manualDatapointDraft.kind,
});
await refreshCollectorData();
await refreshCollectorDebug().catch(() => undefined);
setManualDatapointDraft({ alias: "", address: "", pointIndex: "", unit: "°C", dataType: sourceDraft.protocol === "knx-ip" ? "group-address" : "holding-register", kind: "analog" });
setManualDatapointDraft({ alias: "", address: "", pointIndex: "", unit: "°C", dataType: getDefaultPointDataType(sourceDraft.protocol), kind: "analog" });
setSuccessMessage("Datenpunkt angelegt.");
} catch (error) {
setBootError(error.message);
@@ -1753,6 +1820,12 @@ export default function App() {
<section className="admin-section">
<div className="admin-subnav">
{adminSections.filter((section) => {
if (section.id === "aliases") {
return canEditAliases;
}
if (section.id === "sources") {
return canManageSources || canEditAliases;
}
if (section.id === "users") {
return canManageUsers;
}
@@ -1975,7 +2048,7 @@ export default function App() {
<small className="muted">Zuletzt ausgewählte Quelle: {currentSource?.name || "noch keine"}</small>
</div>
{(sourceDraft.protocol === "modbus-tcp" || sourceDraft.protocol === "knx-ip") ? (
{canManageSources ? (
<div className="protocol-block">
<strong>Einzelner Datenpunkt</strong>
<div className="form-grid compact-forms">
@@ -1984,7 +2057,7 @@ export default function App() {
<input value={manualDatapointDraft.alias} onChange={(event) => setManualDatapointDraft((current) => ({ ...current, alias: event.target.value }))} />
</label>
<label>
<span>{sourceDraft.protocol === "knx-ip" ? "Gruppenadresse" : "Adresse / Register"}</span>
<span>{getManualAddressLabel(sourceDraft.protocol)}</span>
<input value={manualDatapointDraft.address} onChange={(event) => setManualDatapointDraft((current) => ({ ...current, address: event.target.value }))} />
</label>
<label>
@@ -2273,43 +2346,3 @@ export default function App() {
</div>
);
}