feat: OPC-UA Einstellungen in eigenen Redux Slice ausgelagert

- OPC-UA bezogene Variablen aus `variablesSlice` entfernt und in `opcuaSettingsSlice` ausgelagert
- Neue Redux Actions für:
  - `setOpcUaZustand` (OPC-UA Zustand setzen)
  - `setOpcUaEncryption` (Verschlüsselung setzen)
  - `setOpcUaActiveClientCount` (Anzahl aktiver Clients setzen)
  - `setOpcUaNodesetName` (Nodeset-Name setzen)
  - `addOpcUaUser` & `removeOpcUaUser` (Benutzerverwaltung)
- `loadWindowVariables.ts` angepasst, um OPC-UA-Daten in `opcuaSettingsSlice` zu speichern
- Benutzerverwaltung optimiert:
  - Manuell hinzugefügte Benutzer bleiben erhalten
  - Benutzer werden nur aktualisiert, wenn sich `window.win_opcUaUsers` ändert
- Keine automatische Statusumschaltung mehr beim OPC-UA-Server-Button

Jetzt ist die OPC-UA Verwaltung sauber getrennt und stabil! 🚀
This commit is contained in:
Ismail Ali
2025-02-23 11:06:15 +01:00
parent b85c8c67e2
commit 772ef50af5
6 changed files with 256 additions and 62 deletions

View File

@@ -11,7 +11,6 @@ const GeneralSettings = () => {
const systemSettings = useSelector( const systemSettings = useSelector(
(state: RootState) => state.systemSettings (state: RootState) => state.systemSettings
); );
console.log("Redux SystemSettings:", systemSettings);
const [name, setName] = useState(systemSettings.deviceName || ""); const [name, setName] = useState(systemSettings.deviceName || "");
const [ip, setIp] = useState(systemSettings.ip || ""); const [ip, setIp] = useState(systemSettings.ip || "");

View File

@@ -1,57 +1,62 @@
"use client"; // /components/main/settingsPageComponents/OPCUAInterfaceSettings.tsx
import React, { useState } from "react"; import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../../../redux/store";
import {
setOpcUaEncryption,
toggleOpcUaServer,
setOpcUaNodesetName,
addOpcUaUser,
removeOpcUaUser,
} from "../../../redux/slices/opcuaSettingsSlice";
export default function OPCUAInterfaceSettings() { export default function OPCUAInterfaceSettings() {
const [isEnabled, setIsEnabled] = useState(true); const dispatch = useDispatch();
const [encryption, setEncryption] = useState("None"); const opcuaSettings = useSelector((state: RootState) => state.opcuaSettings);
const [users, setUsers] = useState([
{ id: 1, username: "admin", password: "admin123" },
{ id: 2, username: "user1", password: "user123" },
]);
const [newUser, setNewUser] = useState({ username: "", password: "" });
const toggleServer = () => setIsEnabled(!isEnabled); // Lokale Zustände für das neue Benutzerformular
const updateEncryption = (event) => setEncryption(event.target.value); const [newUsername, setNewUsername] = useState("");
const addUser = () => { const [newPassword, setNewPassword] = useState("");
if (newUser.username && newUser.password) { const [nodesetName, setNodesetName] = useState(
setUsers([...users, { id: users.length + 1, ...newUser }]); opcuaSettings.opcUaNodesetName
setNewUser({ username: "", password: "" }); );
const handleAddUser = () => {
if (newUsername.trim() && newPassword.trim()) {
dispatch(addOpcUaUser({ username: newUsername, password: newPassword }));
setNewUsername("");
setNewPassword("");
} }
}; };
const removeUser = (id) => setUsers(users.filter((user) => user.id !== id));
const handleNodesetUpdate = () => {
dispatch(setOpcUaNodesetName(nodesetName));
};
return ( return (
<div className="max-w-3xl mx-auto p-6 bg-gray-100 shadow-md rounded-lg"> <div className="max-w-3xl mx-auto p-4 bg-gray-100 shadow-md rounded-lg">
<h2 className="text-xl font-semibold mb-4">OPCUA Server Einstellungen</h2> <h2 className="text-lg font-semibold mb-4">OPCUA Server Einstellungen</h2>
{/* Server Aktivierung */} {/* Server Aktivierung */}
<div className="mb-4"> <div className="mb-4 flex items-center">
<label className="flex items-center cursor-pointer"> <label className="mr-4 font-medium">Server Status:</label>
<input <button
type="checkbox" onClick={() => dispatch(toggleOpcUaServer())}
checked={isEnabled} className={`px-4 py-2 rounded ${
onChange={toggleServer} opcuaSettings.isEnabled ? "bg-green-500" : "bg-gray-300"
className="hidden" } text-white`}
/> >
<div {opcuaSettings.isEnabled ? "Aktiviert" : "Deaktiviert"}
className={`w-12 h-6 rounded-full transition-all ${ </button>
isEnabled ? "bg-green-500" : "bg-gray-300"
}`}
></div>
<span className="ml-2 text-sm">
{isEnabled ? "Aktiviert" : "Deaktiviert"}
</span>
</label>
</div> </div>
{/* Verschlüsselung */} {/* Verschlüsselung */}
<div className="mb-4"> <div className="mb-4">
<label className="block text-sm font-medium text-gray-700"> <label className="block font-medium mb-1">Verschlüsselung</label>
Verschlüsselung
</label>
<select <select
value={encryption} value={opcuaSettings.encryption}
onChange={updateEncryption} onChange={(e) => dispatch(setOpcUaEncryption(e.target.value))}
className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring focus:ring-blue-200" className="w-full p-2 border border-gray-300 rounded-md"
> >
<option value="None">Keine</option> <option value="None">Keine</option>
<option value="Basic256">Basic256</option> <option value="Basic256">Basic256</option>
@@ -59,11 +64,46 @@ export default function OPCUAInterfaceSettings() {
</select> </select>
</div> </div>
{/* Benutzer & Passwörter */} {/* ✅ OPCUA Zustand (nur Anzeige) */}
<div className="mb-4">
<label className="block font-medium mb-1">OPCUA Zustand</label>
<div className="p-2 border border-gray-300 rounded-md bg-white">
{opcuaSettings.opcUaZustand}
</div>
</div>
{/* ✅ Aktive Clients (nur Anzeige) */}
<div className="mb-4">
<label className="block font-medium mb-1">Aktive Clients</label>
<div className="p-2 border border-gray-300 rounded-md bg-white">
{opcuaSettings.opcUaActiveClientCount}
</div>
</div>
{/* ✅ Nodeset Name */}
<div className="mb-4">
<label className="block font-medium mb-1">Nodeset Name</label>
<div className="flex">
<input
type="text"
className="flex-grow p-2 border border-gray-300 rounded-l-md"
value={nodesetName}
onChange={(e) => setNodesetName(e.target.value)}
/>
<button
onClick={handleNodesetUpdate}
className="px-4 py-2 bg-blue-500 text-white rounded-r-md"
>
Übernehmen
</button>
</div>
</div>
{/* ✅ Benutzerverwaltung */}
<div className="mb-4"> <div className="mb-4">
<h3 className="text-lg font-semibold mb-2">Benutzer</h3> <h3 className="text-lg font-semibold mb-2">Benutzer</h3>
<ul className="space-y-2"> <ul className="space-y-2">
{users.map((user) => ( {opcuaSettings.users.map((user) => (
<li <li
key={user.id} key={user.id}
className="p-2 bg-white shadow-sm rounded-md flex justify-between items-center" className="p-2 bg-white shadow-sm rounded-md flex justify-between items-center"
@@ -73,36 +113,34 @@ export default function OPCUAInterfaceSettings() {
(Passwort: {user.password}) (Passwort: {user.password})
</span> </span>
<button <button
onClick={() => removeUser(user.id)} onClick={() => dispatch(removeOpcUaUser(user.id))}
className="ml-4 text-red-500" className="text-red-500"
> >
Löschen Löschen
</button> </button>
</li> </li>
))} ))}
</ul> </ul>
<div className="mt-4">
{/* ✅ Neuen Benutzer hinzufügen */}
<div className="mt-4 flex space-x-2">
<input <input
type="text" type="text"
placeholder="Benutzername" placeholder="Benutzername"
value={newUser.username} value={newUsername}
onChange={(e) => onChange={(e) => setNewUsername(e.target.value)}
setNewUser({ ...newUser, username: e.target.value }) className="p-2 border rounded w-1/3"
}
className="p-2 border rounded mr-2"
/> />
<input <input
type="password" type="password"
placeholder="Passwort" placeholder="Passwort"
value={newUser.password} value={newPassword}
onChange={(e) => onChange={(e) => setNewPassword(e.target.value)}
setNewUser({ ...newUser, password: e.target.value }) className="p-2 border rounded w-1/3"
}
className="p-2 border rounded mr-2"
/> />
<button <button
onClick={addUser} onClick={handleAddUser}
className="bg-blue-500 text-white p-2 rounded" className="bg-blue-500 text-white p-2 rounded w-1/3"
> >
Hinzufügen Hinzufügen
</button> </button>

View File

@@ -6,5 +6,5 @@
2: Patch oder Hotfix (Bugfixes oder kleine Änderungen). 2: Patch oder Hotfix (Bugfixes oder kleine Änderungen).
*/ */
const webVersion = "1.6.91"; const webVersion = "1.6.92";
export default webVersion; export default webVersion;

View File

@@ -0,0 +1,76 @@
// redux/slices/opcuaSettingsSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface OPCUAUser {
id: number;
username: string;
password: string;
}
interface OPCUASettingsState {
isEnabled: boolean;
encryption: string;
opcUaZustand: string;
opcUaActiveClientCount: number;
opcUaNodesetName: string;
users: OPCUAUser[];
}
const initialState: OPCUASettingsState = {
isEnabled: true,
encryption: "None",
opcUaZustand: "Offline",
opcUaActiveClientCount: 0,
opcUaNodesetName: "DefaultNodeset",
users: [
{ id: 1, username: "admin", password: "admin123" },
{ id: 2, username: "user1", password: "user123" },
],
};
const opcuaSettingsSlice = createSlice({
name: "opcuaSettings",
initialState,
reducers: {
toggleOpcUaServer(state) {
state.isEnabled = !state.isEnabled;
},
setOpcUaEncryption(state, action: PayloadAction<string>) {
state.encryption = action.payload;
},
setOpcUaZustand(state, action: PayloadAction<string>) {
state.opcUaZustand = action.payload;
},
setOpcUaActiveClientCount(state, action: PayloadAction<number>) {
state.opcUaActiveClientCount = action.payload;
},
setOpcUaNodesetName(state, action: PayloadAction<string>) {
state.opcUaNodesetName = action.payload;
},
addOpcUaUser(
state,
action: PayloadAction<{ username: string; password: string }>
) {
const newUser = {
id: state.users.length + 1,
...action.payload,
};
state.users.push(newUser);
},
removeOpcUaUser(state, action: PayloadAction<number>) {
state.users = state.users.filter((user) => user.id !== action.payload);
},
},
});
export const {
toggleOpcUaServer,
setOpcUaEncryption,
setOpcUaZustand,
setOpcUaActiveClientCount,
setOpcUaNodesetName,
addOpcUaUser,
removeOpcUaUser,
} = opcuaSettingsSlice.actions;
export default opcuaSettingsSlice.reducer;

View File

@@ -8,6 +8,7 @@ import digitalInputsReducer from "./slices/digitalInputsSlice";
import kabelueberwachungChartReducer from "./slices/kabelueberwachungChartSlice"; import kabelueberwachungChartReducer from "./slices/kabelueberwachungChartSlice";
import dashboardReducer from "./slices/dashboardSlice"; import dashboardReducer from "./slices/dashboardSlice";
import systemSettingsReducer from "./slices/systemSettingsSlice"; import systemSettingsReducer from "./slices/systemSettingsSlice";
import opcuaSettingsReducer from "./slices/opcuaSettingsSlice";
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
@@ -19,6 +20,7 @@ const store = configureStore({
kabelueberwachungChart: kabelueberwachungChartReducer, kabelueberwachungChart: kabelueberwachungChartReducer,
dashboard: dashboardReducer, dashboard: dashboardReducer,
systemSettings: systemSettingsReducer, systemSettings: systemSettingsReducer,
opcuaSettings: opcuaSettingsReducer,
}, },
}); });

View File

@@ -1,5 +1,15 @@
// /utils/loadWindowVariables.ts
import store from "../redux/store"; import store from "../redux/store";
import { setSystemSettings } from "../redux/slices/systemSettingsSlice"; import { setSystemSettings } from "../redux/slices/systemSettingsSlice";
import {
toggleOpcUaServer,
setOpcUaEncryption,
setOpcUaZustand,
setOpcUaActiveClientCount,
setOpcUaNodesetName,
addOpcUaUser,
removeOpcUaUser,
} from "../redux/slices/opcuaSettingsSlice";
// ✅ Interface für `window`-Objekt zur TypeScript-Sicherheit // ✅ Interface für `window`-Objekt zur TypeScript-Sicherheit
interface CustomWindow extends Window { interface CustomWindow extends Window {
@@ -21,6 +31,16 @@ interface SystemSettings {
ntpActive: boolean; ntpActive: boolean;
} }
// ✅ Interface für OPC-UA Einstellungen im Redux-Store
interface OpcUaSettings {
isEnabled: boolean;
encryption: string;
opcUaZustand: string;
opcUaActiveClientCount: number;
opcUaNodesetName: string;
users: { id: number; username: string; password: string }[];
}
// ✅ Hauptfunktion zum Laden von `window`-Variablen // ✅ Hauptfunktion zum Laden von `window`-Variablen
export async function loadWindowVariables(): Promise<Record<string, any>> { export async function loadWindowVariables(): Promise<Record<string, any>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -73,11 +93,13 @@ export async function loadWindowVariables(): Promise<Record<string, any>> {
"win_analogeEingaenge6", "win_analogeEingaenge6",
"win_analogeEingaenge7", "win_analogeEingaenge7",
"win_analogeEingaenge8", "win_analogeEingaenge8",
"win_da_state",
"win_da_bezeichnung",
"win_opcUaZustand", "win_opcUaZustand",
"win_opcUaActiveClientCount", "win_opcUaActiveClientCount",
"win_opcUaNodesetName", "win_opcUaNodesetName",
"win_da_state", "win_opcUaEncryption", // ✅ NEU: Verschlüsselung von OPC-UA
"win_da_bezeichnung", "win_opcUaUsers", // ✅ NEU: OPC-UA Benutzerliste
]; ];
const scripts: string[] = [ const scripts: string[] = [
@@ -127,6 +149,7 @@ export async function loadWindowVariables(): Promise<Record<string, any>> {
// ✅ Redux mit Systemvariablen aktualisieren // ✅ Redux mit Systemvariablen aktualisieren
loadAndStoreSystemSettings(win); loadAndStoreSystemSettings(win);
loadAndStoreOpcUaSettings(win);
resolve(variablesObj); resolve(variablesObj);
}) })
@@ -155,3 +178,59 @@ const loadAndStoreSystemSettings = (win: CustomWindow) => {
store.dispatch(setSystemSettings(settings)); store.dispatch(setSystemSettings(settings));
}; };
// ✅ Funktion zum Speichern von OPC-UA Variablen in Redux
const loadAndStoreOpcUaSettings = (win: CustomWindow) => {
// ✅ Aktuellen Redux-Status holen
const currentState = store.getState().opcuaSettings;
// ✅ Nur setzen, wenn sich der Zustand geändert hat
if (currentState.opcUaZustand !== win.win_opcUaZustand) {
store.dispatch(setOpcUaZustand(win.win_opcUaZustand || "Offline"));
}
// ✅ Nur setzen, wenn sich die Verschlüsselung geändert hat
if (currentState.encryption !== win.win_opcUaEncryption) {
store.dispatch(setOpcUaEncryption(win.win_opcUaEncryption || "None"));
}
// ✅ Nur setzen, wenn sich die Anzahl der aktiven Clients geändert hat
if (currentState.opcUaActiveClientCount !== win.win_opcUaActiveClientCount) {
store.dispatch(
setOpcUaActiveClientCount(win.win_opcUaActiveClientCount || 0)
);
}
// ✅ Nur setzen, wenn sich der Nodeset-Name geändert hat
if (currentState.opcUaNodesetName !== win.win_opcUaNodesetName) {
store.dispatch(
setOpcUaNodesetName(win.win_opcUaNodesetName || "DefaultNodeset")
);
}
// ✅ Benutzer synchronisieren (aber nicht überschreiben, wenn manuell hinzugefügt)
if (Array.isArray(win.win_opcUaUsers) && win.win_opcUaUsers.length > 0) {
const newUsers = win.win_opcUaUsers;
const currentUsers = currentState.users;
// ✅ Vorhandene Benutzer entfernen, die nicht mehr in `window` sind
currentUsers.forEach((user) => {
if (!newUsers.some((newUser) => newUser.username === user.username)) {
store.dispatch(removeOpcUaUser(user.id));
}
});
// ✅ Nur neue Benutzer hinzufügen, falls sie nicht existieren
newUsers.forEach((user) => {
if (
!currentUsers.some(
(existingUser) => existingUser.username === user.username
)
) {
store.dispatch(
addOpcUaUser({ username: user.username, password: user.password })
);
}
});
}
};