Fixed mail bug with static variable, fixed token encoding

This commit is contained in:
Noe 2023-10-26 16:38:37 +02:00
parent 76099131c2
commit 0a91445ddd
50 changed files with 1964 additions and 924 deletions

View File

@ -456,9 +456,9 @@ public class Controller : ControllerBase
} }
[HttpPost(nameof(ResetPasswordRequest))] [HttpPost(nameof(ResetPasswordRequest))]
public async Task<ActionResult<IEnumerable<Object>>> ResetPasswordRequest(String email) public async Task<ActionResult<IEnumerable<Object>>> ResetPasswordRequest(String username)
{ {
var user = Db.GetUserByEmail(email); var user = Db.GetUserByEmail(username);
if (user is null) if (user is null)
return Unauthorized(); return Unauthorized();
@ -482,7 +482,7 @@ public class Controller : ControllerBase
Db.DeleteUserPassword(user); Db.DeleteUserPassword(user);
return Redirect($"https://monitor.innov.energy/?username={user.Email}&reset=true"); return Redirect($"https://monnitor.innov.energy/?username={user.Email}&reset=true"); // TODO: move to settings file
} }
} }

View File

@ -1,4 +1,5 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Web;
using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.Database;
using InnovEnergy.Lib.Mailer; using InnovEnergy.Lib.Mailer;
using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils;
@ -218,11 +219,12 @@ public static class UserMethods
public static Task SendPasswordResetEmail(this User user, String token) public static Task SendPasswordResetEmail(this User user, String token)
{ {
const String subject = "Reset the password of your InnovEnergy-Account"; const String subject = "Reset the password of your InnovEnergy-Account";
const String resetLink = "https://monitor.innov.energy/api/ResetPassword"; // TODO: move to settings file const String resetLink = "https://monnitor.innov.energy/api/ResetPassword"; // TODO: move to settings file
var encodedToken = HttpUtility.UrlEncode(token);
var body = $"Dear {user.Name}\n" + var body = $"Dear {user.Name}\n" +
$"To reset your password " + $"To reset your password " +
$"please open this link:{resetLink}?token={token}"; $"please open this link:{resetLink}?token={encodedToken}";
return user.SendEmail(subject, body); return user.SendEmail(subject, body);
} }

View File

@ -7,7 +7,7 @@
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchUrl": "swagger", "launchUrl": "swagger",
"applicationUrl": "https://localhost:7087", "applicationUrl": "http://localhost:7087",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_ENVIRONMENT": "Development",
"HOME":"~/backend" "HOME":"~/backend"

View File

@ -21,7 +21,7 @@
"chart.js": "^4.4.0", "chart.js": "^4.4.0",
"clsx": "1.1.1", "clsx": "1.1.1",
"cytoscape": "^3.26.0", "cytoscape": "^3.26.0",
"date-fns": "2.28.0", "date-fns": "^2.28.0",
"history": "5.3.0", "history": "5.3.0",
"linq-to-typescript": "^11.0.0", "linq-to-typescript": "^11.0.0",
"nprogress": "0.2.0", "nprogress": "0.2.0",
@ -38,9 +38,11 @@
"react-icons": "^4.11.0", "react-icons": "^4.11.0",
"react-icons-converter": "^1.1.4", "react-icons-converter": "^1.1.4",
"react-intl": "^6.4.4", "react-intl": "^6.4.4",
"react-redux": "^8.1.3",
"react-router": "6.3.0", "react-router": "6.3.0",
"react-router-dom": "6.3.0", "react-router-dom": "6.3.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"redux": "^4.2.1",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"simplytyped": "^3.3.0", "simplytyped": "^3.3.0",
"stylis": "4.1.1", "stylis": "4.1.1",
@ -4662,6 +4664,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
}, },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
},
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@ -15155,6 +15162,49 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
}, },
"node_modules/react-redux": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz",
"integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==",
"dependencies": {
"@babel/runtime": "^7.12.1",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/use-sync-external-store": "^0.0.3",
"hoist-non-react-statics": "^3.3.2",
"react-is": "^18.0.0",
"use-sync-external-store": "^1.0.0"
},
"peerDependencies": {
"@types/react": "^16.8 || ^17.0 || ^18.0",
"@types/react-dom": "^16.8 || ^17.0 || ^18.0",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0",
"react-native": ">=0.59",
"redux": "^4 || ^5.0.0-beta.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-redux/node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@ -15334,6 +15384,14 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"dependencies": {
"@babel/runtime": "^7.9.2"
}
},
"node_modules/regenerate": { "node_modules/regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@ -17185,6 +17243,14 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -21400,6 +21466,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
}, },
"@types/use-sync-external-store": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
},
"@types/ws": { "@types/ws": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@ -28886,6 +28957,26 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
}, },
"react-redux": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz",
"integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==",
"requires": {
"@babel/runtime": "^7.12.1",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/use-sync-external-store": "^0.0.3",
"hoist-non-react-statics": "^3.3.2",
"react-is": "^18.0.0",
"use-sync-external-store": "^1.0.0"
},
"dependencies": {
"react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
}
}
},
"react-refresh": { "react-refresh": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@ -29020,6 +29111,14 @@
} }
} }
}, },
"redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"requires": {
"@babel/runtime": "^7.9.2"
}
},
"regenerate": { "regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@ -30399,6 +30498,12 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"requires": {}
},
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -17,7 +17,7 @@
"chart.js": "^4.4.0", "chart.js": "^4.4.0",
"clsx": "1.1.1", "clsx": "1.1.1",
"cytoscape": "^3.26.0", "cytoscape": "^3.26.0",
"date-fns": "2.28.0", "date-fns": "^2.28.0",
"history": "5.3.0", "history": "5.3.0",
"linq-to-typescript": "^11.0.0", "linq-to-typescript": "^11.0.0",
"nprogress": "0.2.0", "nprogress": "0.2.0",
@ -34,9 +34,11 @@
"react-icons": "^4.11.0", "react-icons": "^4.11.0",
"react-icons-converter": "^1.1.4", "react-icons-converter": "^1.1.4",
"react-intl": "^6.4.4", "react-intl": "^6.4.4",
"react-redux": "^8.1.3",
"react-router": "6.3.0", "react-router": "6.3.0",
"react-router-dom": "6.3.0", "react-router-dom": "6.3.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"redux": "^4.2.1",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"simplytyped": "^3.3.0", "simplytyped": "^3.3.0",
"stylis": "4.1.1", "stylis": "4.1.1",

View File

@ -1,4 +1,4 @@
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
import { CssBaseline } from '@mui/material'; import { CssBaseline } from '@mui/material';
import ThemeProvider from './theme/ThemeProvider'; import ThemeProvider from './theme/ThemeProvider';
import React, { lazy, Suspense, useContext, useState } from 'react'; import React, { lazy, Suspense, useContext, useState } from 'react';
@ -9,15 +9,16 @@ import en from './lang/en.json';
import de from './lang/de.json'; import de from './lang/de.json';
import fr from './lang/fr.json'; import fr from './lang/fr.json';
import SuspenseLoader from './components/SuspenseLoader'; import SuspenseLoader from './components/SuspenseLoader';
import { RouteObject } from 'react-router';
import BaseLayout from './layouts/BaseLayout';
import SidebarLayout from './layouts/SidebarLayout'; import SidebarLayout from './layouts/SidebarLayout';
import { TokenContext } from './contexts/tokenContext'; import { TokenContext } from './contexts/tokenContext';
import ResetPassword from './components/ResetPassword'; import ResetPassword from './components/ResetPassword';
import ForgotPassword from './components/ForgotPassword';
import InstallationTabs from './content/dashboards/Installations/index'; import InstallationTabs from './content/dashboards/Installations/index';
import routes from 'src/Resources/routes.json'; import routes from 'src/Resources/routes.json';
import './App.css'; import './App.css';
import ForgotPassword from './components/ForgotPassword';
import { axiosConfigWithoutToken } from './Resources/axiosConfig';
import UsersContextProvider from './contexts/UsersContextProvider';
import InstallationsContextProvider from './contexts/InstallationsContextProvider';
function App() { function App() {
const context = useContext(UserContext); const context = useContext(UserContext);
@ -25,6 +26,9 @@ function App() {
const tokencontext = useContext(TokenContext); const tokencontext = useContext(TokenContext);
const { token, setNewToken, removeToken } = tokencontext; const { token, setNewToken, removeToken } = tokencontext;
const [forgotPassword, setForgotPassword] = useState(false); const [forgotPassword, setForgotPassword] = useState(false);
const navigate = useNavigate();
const searchParams = new URLSearchParams(location.search);
const username = searchParams.get('username');
const [language, setLanguage] = useState('en'); const [language, setLanguage] = useState('en');
const getTranslations = () => { const getTranslations = () => {
@ -62,9 +66,26 @@ function App() {
lazy(() => import('src/components/ResetPassword')) lazy(() => import('src/components/ResetPassword'))
); );
const SetNewPassword = Loader(
lazy(() => import('src/components/SetNewPassword'))
);
const Login = Loader(lazy(() => import('src/components/login'))); const Login = Loader(lazy(() => import('src/components/login')));
const Users = Loader(lazy(() => import('src/content/dashboards/Users'))); const Users = Loader(lazy(() => import('src/content/dashboards/Users')));
const loginToResetPassword = () => {
axiosConfigWithoutToken
.post('/Login', null, { params: { username, password: '' } })
.then((response) => {
if (response.data && response.data.token) {
setNewToken(response.data.token);
setUser(response.data.user);
navigate(routes.installations);
}
})
.catch((error) => {});
};
// Status // Status
const Status404 = Loader( const Status404 = Loader(
lazy(() => import('src/content/pages/Status/Status404')) lazy(() => import('src/content/pages/Status/Status404'))
@ -79,61 +100,25 @@ function App() {
lazy(() => import('src/content/pages/Status/Maintenance')) lazy(() => import('src/content/pages/Status/Maintenance'))
); );
const routesArray: RouteObject[] = [ if (username) {
{ loginToResetPassword();
path: '',
element: <BaseLayout />,
children: [
{
path: '/',
element: <Navigate to="installations/" replace />
},
{
path: 'status',
children: [
{
path: '',
element: <Navigate to="404" replace />
},
{
path: '404',
element: <Status404 />
},
{
path: '500',
element: <Status500 />
},
{
path: 'maintenance',
element: <StatusMaintenance />
},
{
path: 'coming-soon',
element: <StatusComingSoon />
}
]
},
{
path: '*',
element: <Status404 />
}
]
}
];
if (forgotPassword) {
return (
<ThemeProvider>
<CssBaseline />
<ForgotPassword resetPassword={resetPassword} />
</ThemeProvider>
);
} }
if (!token) { if (!token) {
return ( return (
<ThemeProvider> <ThemeProvider>
<CssBaseline /> <CssBaseline />
<Login onForgotPassword={onForgotPassword}></Login> <Routes>
<Route
path={''}
element={<Navigate to={routes.login}></Navigate>}
></Route>
<Route path={routes.login} element={<Login></Login>}></Route>
<Route
path={routes.forgotPassword}
element={<ForgotPassword />}
></Route>
</Routes>
</ThemeProvider> </ThemeProvider>
); );
} }
@ -142,7 +127,7 @@ function App() {
return ( return (
<ThemeProvider> <ThemeProvider>
<CssBaseline /> <CssBaseline />
<ResetPassword></ResetPassword> <SetNewPassword></SetNewPassword>
</ThemeProvider> </ThemeProvider>
); );
} }
@ -156,18 +141,10 @@ function App() {
> >
<CssBaseline /> <CssBaseline />
<Routes> <Routes>
{routesArray.map((route, index) => ( <Route
<Route key={index} path={route.path} element={route.element}> path={''}
{route.children && element={<Navigate to={routes.installations}></Navigate>}
route.children.map((childRoute, childIndex) => ( ></Route>
<Route
key={childIndex}
path={childRoute.path}
element={childRoute.element}
/>
))}
</Route>
))}
<Route <Route
path="/" path="/"
element={ element={
@ -179,11 +156,18 @@ function App() {
> >
<Route <Route
path={routes.installations + '*'} path={routes.installations + '*'}
element={<InstallationTabs />} element={
<UsersContextProvider>
<InstallationsContextProvider>
<InstallationTabs />
</InstallationsContextProvider>
</UsersContextProvider>
}
/> />
<Route path={routes.users + '*'} element={<Users />} /> <Route path={routes.users + '*'} element={<Users />} />
<Route path="ResetPassword" element={<ResetPassword />}></Route> <Route path="ResetPassword" element={<ResetPassword />}></Route>
<Route path="Login" element={<Login />}></Route>
</Route> </Route>
</Routes> </Routes>
</IntlProvider> </IntlProvider>

View File

@ -37,7 +37,7 @@ export function findPower(value) {
value = Math.abs(value); value = Math.abs(value);
// Calculate the power of 10 that's greater or equal to the absolute value // Calculate the power of 10 that's greater or equal to the absolute value
let exponent = Math.floor(Math.log10(value)); const exponent = Math.floor(Math.log10(value));
// Compute the nearest power of 10 // Compute the nearest power of 10
const nearestPowerOf10 = Math.pow(10, exponent); const nearestPowerOf10 = Math.pow(10, exponent);

View File

@ -1,6 +1,6 @@
{ {
"installation": "installation/", "installation": "installation/",
"liveView": "liveView/", "live": "live",
"users": "/users/", "users": "/users/",
"log": "log/", "log": "log/",
"installations": "/installations/", "installations": "/installations/",
@ -9,6 +9,13 @@
"folder": "folder/", "folder": "folder/",
"manageAccess": "manageAccess/", "manageAccess": "manageAccess/",
"user": "user/", "user": "user/",
"tree": "tree", "tree": "tree/",
"list": "list" "list": "list/",
"overview": "overview",
"manage": "manage",
"log": "log",
"information": "information",
"configuration": "configuration",
"login": "/login/",
"forgotPassword": "/forgotPassword/"
} }

View File

@ -16,12 +16,14 @@ import { TokenContext } from 'src/contexts/tokenContext';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import axiosConfig from 'src/Resources/axiosConfig'; import axiosConfig from 'src/Resources/axiosConfig';
import { useNavigate } from 'react-router-dom';
import routes from 'src/Resources/routes.json';
interface ForgotPasswordPromps { interface ForgotPasswordPromps {
resetPassword: () => void; resetPassword: () => void;
} }
function ForgotPassword(props: ForgotPasswordPromps) { function ForgotPassword() {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -35,6 +37,7 @@ function ForgotPassword(props: ForgotPasswordPromps) {
const { currentUser, setUser, removeUser } = context; const { currentUser, setUser, removeUser } = context;
const tokencontext = useContext(TokenContext); const tokencontext = useContext(TokenContext);
const { token, setNewToken, removeToken } = tokencontext; const { token, setNewToken, removeToken } = tokencontext;
const navigate = useNavigate();
const handleUsernameChange = (e) => { const handleUsernameChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
@ -43,7 +46,8 @@ function ForgotPassword(props: ForgotPasswordPromps) {
const handleReturn = () => { const handleReturn = () => {
setOpen(false); setOpen(false);
props.resetPassword(); navigate(routes.login);
//props.resetPassword();
}; };
const handleSubmit = () => { const handleSubmit = () => {
@ -72,7 +76,7 @@ function ForgotPassword(props: ForgotPasswordPromps) {
<Container maxWidth="xl" sx={{ pt: 2 }}> <Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container> <Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}> <Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://www.innov.energy/de/"> <a href="https://monitor.innov.energy/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" /> <img src={innovenergyLogo} alt="innovenergy logo" height="100" />
</a> </a>
</Grid> </Grid>
@ -122,6 +126,12 @@ function ForgotPassword(props: ForgotPasswordPromps) {
margin="normal" margin="normal"
required required
sx={{ width: 350 }} sx={{ width: 350 }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSubmit();
}
}}
/> />
{loading && <CircularProgress sx={{ color: '#ffc04d' }} />} {loading && <CircularProgress sx={{ color: '#ffc04d' }} />}

View File

@ -73,7 +73,7 @@ function ResetPassword() {
<Container maxWidth="xl" sx={{ pt: 2 }}> <Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container> <Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}> <Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://www.innov.energy/de/"> <a href="https://monitor.innov.energy/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" /> <img src={innovenergyLogo} alt="innovenergy logo" height="100" />
</a> </a>
</Grid> </Grid>

View File

@ -23,12 +23,9 @@ import { TokenContext } from 'src/contexts/tokenContext';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox'; import CheckBoxIcon from '@mui/icons-material/CheckBox';
import routes from 'src/Resources/routes.json';
interface loginPromps { function Login() {
onForgotPassword: () => void;
}
function Login(props: loginPromps) {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -43,11 +40,10 @@ function Login(props: loginPromps) {
if (!context) { if (!context) {
return null; return null;
} }
const { currentUser, setUser, removeUser } = context;
const { currentUser, setUser, removeUser } = context;
const tokencontext = useContext(TokenContext); const tokencontext = useContext(TokenContext);
const { token, setNewToken, removeToken } = tokencontext; const { token, setNewToken, removeToken } = tokencontext;
const cookies = new Cookies(); const cookies = new Cookies();
const handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@ -61,6 +57,11 @@ function Login(props: loginPromps) {
const handleRememberMeChange = () => { const handleRememberMeChange = () => {
setRememberMe(!rememberMe); setRememberMe(!rememberMe);
}; };
const onForgotPassword = () => {
navigate(routes.forgotPassword);
};
const handleSubmit = () => { const handleSubmit = () => {
setLoading(true); setLoading(true);
axiosConfigWithoutToken axiosConfigWithoutToken
@ -76,7 +77,7 @@ function Login(props: loginPromps) {
cookies.set('rememberedUsername', username, { path: '/' }); cookies.set('rememberedUsername', username, { path: '/' });
cookies.set('rememberedPassword', password, { path: '/' }); cookies.set('rememberedPassword', password, { path: '/' });
} }
navigate('/'); navigate(routes.installations);
} }
}) })
.catch((error) => { .catch((error) => {
@ -91,10 +92,10 @@ function Login(props: loginPromps) {
return ( return (
<> <>
<Container maxWidth="xl" sx={{ pt: 2 }}> <Container maxWidth="xl" sx={{ pt: 2 }} className="login">
<Grid container> <Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}> <Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://www.innov.energy/de/"> <a href="https://monitor.innov.energy/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" /> <img src={innovenergyLogo} alt="innovenergy logo" height="100" />
</a> </a>
</Grid> </Grid>
@ -113,7 +114,6 @@ function Login(props: loginPromps) {
boxShadow: 24, boxShadow: 24,
p: 6, p: 6,
position: 'absolute', position: 'absolute',
top: '30%', top: '30%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)' transform: 'translate(-50%, -50%)'
@ -143,6 +143,12 @@ function Login(props: loginPromps) {
margin="normal" margin="normal"
required required
sx={{ width: 350 }} sx={{ width: 350 }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSubmit();
}
}}
/> />
<TextField <TextField
@ -155,6 +161,12 @@ function Login(props: loginPromps) {
margin="normal" margin="normal"
required required
sx={{ width: 350 }} sx={{ width: 350 }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSubmit();
}
}}
/> />
<FormControlLabel <FormControlLabel
@ -188,7 +200,7 @@ function Login(props: loginPromps) {
{loading && ( {loading && (
<CircularProgress <CircularProgress
sx={{ sx={{
color: theme.palette.primary.main, color: '#ffc04d',
marginLeft: '20px' marginLeft: '20px'
}} }}
/> />
@ -242,7 +254,7 @@ function Login(props: loginPromps) {
sx={{ color: '#111111' }} sx={{ color: '#111111' }}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
props.onForgotPassword(); onForgotPassword();
}} }}
> >
Forgot password? Forgot password?

View File

@ -1,6 +1,7 @@
import { TopologyValues } from '../Log/graph.util'; import { TopologyValues } from '../Log/graph.util';
import { Box, CardContent, Container, Grid, TextField } from '@mui/material'; import { Box, CardContent, Container, Grid, TextField } from '@mui/material';
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl';
interface ConfigurationProps { interface ConfigurationProps {
values: TopologyValues; values: TopologyValues;
@ -32,7 +33,12 @@ function Configuration(props: ConfigurationProps) {
> >
<div> <div>
<TextField <TextField
label="Minimum SoC" label={
<FormattedMessage
id="minimum_soc"
defaultMessage="Minimum SoC"
/>
}
value={props.values.minimumSoC.values[0].value + ' %'} value={props.values.minimumSoC.values[0].value + ' %'}
fullWidth fullWidth
/> />
@ -40,14 +46,24 @@ function Configuration(props: ConfigurationProps) {
<div> <div>
<TextField <TextField
label="Calibration Charge forced" label={
<FormattedMessage
id="calibration_charge_forced"
defaultMessage="Calibration Charge forced"
/>
}
value={props.values.calibrationChargeForced.values[0].value} value={props.values.calibrationChargeForced.values[0].value}
fullWidth fullWidth
/> />
</div> </div>
<div> <div>
<TextField <TextField
label="Grid Set Point" label={
<FormattedMessage
id="grid_set_point"
defaultMessage="Grid Set Point"
/>
}
value={ value={
( (
(props.values.gridSetPoint.values[0].value as number) / (props.values.gridSetPoint.values[0].value as number) /
@ -59,7 +75,12 @@ function Configuration(props: ConfigurationProps) {
</div> </div>
<div> <div>
<TextField <TextField
label="Installed Power DC1010" label={
<FormattedMessage
id="Installed_Power_DC1010"
defaultMessage="Installed Power DC1010"
/>
}
value={ value={
( (
(props.values.installedDcDcPower.values[0] (props.values.installedDcDcPower.values[0]
@ -71,7 +92,12 @@ function Configuration(props: ConfigurationProps) {
</div> </div>
<div> <div>
<TextField <TextField
label="Maximum Discharge Power" label={
<FormattedMessage
id="Maximum_Discharge_Power"
defaultMessage="Maximum Discharge Power"
/>
}
value={ value={
( (
(props.values.maximumDischargePower.values[0] (props.values.maximumDischargePower.values[0]
@ -83,7 +109,12 @@ function Configuration(props: ConfigurationProps) {
</div> </div>
<div> <div>
<TextField <TextField
label="Number of Batteries" label={
<FormattedMessage
id="Number_of_Batteries"
defaultMessage="Number of Batteries"
/>
}
value={props.values.battery.values.length - 4} value={props.values.battery.values.length - 4}
fullWidth fullWidth
/> />

View File

@ -18,7 +18,6 @@ import CancelIcon from '@mui/icons-material/Cancel';
import { LogContext } from 'src/contexts/LogContextProvider'; import { LogContext } from 'src/contexts/LogContextProvider';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import routes from 'src/Resources/routes.json';
interface FlatInstallationViewProps { interface FlatInstallationViewProps {
installations: I_Installation[]; installations: I_Installation[];
@ -29,7 +28,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
const logContext = useContext(LogContext); const logContext = useContext(LogContext);
const { getStatus } = logContext; const { getStatus } = logContext;
const navigate = useNavigate(); const navigate = useNavigate();
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation')); const installationId = parseInt(searchParams.get('installation'));
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1); const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
@ -37,15 +35,9 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
const handleSelectOneInstallation = (installationID: number): void => { const handleSelectOneInstallation = (installationID: number): void => {
if (selectedInstallation != installationID) { if (selectedInstallation != installationID) {
setSelectedInstallation(installationID); setSelectedInstallation(installationID);
navigate( navigate(`?installation=${installationID}`, {
routes.installations + replace: true
routes.list + });
'?installation=' +
installationID.toString(),
{
replace: true
}
);
} else { } else {
setSelectedInstallation(-1); setSelectedInstallation(-1);
} }
@ -88,8 +80,8 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
</TableCell> </TableCell>
<TableCell> <TableCell>
<FormattedMessage <FormattedMessage
id="order" id="orderNumbers"
defaultMessage="Order Values" defaultMessage="Order Numbers"
/> />
</TableCell> </TableCell>
<TableCell> <TableCell>
@ -234,6 +226,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
</TableContainer> </TableContainer>
</Card> </Card>
</Grid> </Grid>
{props.installations.map((installation) => ( {props.installations.map((installation) => (
<Installation <Installation
key={installation.id} key={installation.id}

View File

@ -1,4 +1,4 @@
import React, { ChangeEvent, useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { import {
Alert, Alert,
Box, Box,
@ -8,9 +8,9 @@ import {
Container, Container,
Grid, Grid,
IconButton, IconButton,
Tab, Modal,
Tabs,
TextField, TextField,
Typography,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material'; import { Close as CloseIcon } from '@mui/icons-material';
@ -18,7 +18,6 @@ import { I_Installation } from 'src/interfaces/InstallationTypes';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { TokenContext } from 'src/contexts/tokenContext'; import { TokenContext } from 'src/contexts/tokenContext';
import { UserContext } from 'src/contexts/userContext'; import { UserContext } from 'src/contexts/userContext';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import AccessContextProvider from 'src/contexts/AccessContextProvider'; import AccessContextProvider from 'src/contexts/AccessContextProvider';
import Access from '../ManageAccess/Access'; import Access from '../ManageAccess/Access';
@ -38,43 +37,12 @@ import Configuration from '../Configuration/Configuration';
import { fetchData } from 'src/content/dashboards/Installations/fetchData'; import { fetchData } from 'src/content/dashboards/Installations/fetchData';
interface singleInstallationProps { interface singleInstallationProps {
current_installation: I_Installation; current_installation?: I_Installation;
type: string; type?: string;
} }
function Installation(props: singleInstallationProps) { function Installation(props: singleInstallationProps) {
const tabs = [
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
,
{
value: 'manage',
label: <FormattedMessage id="manage" defaultMessage="Access Management" />
},
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'information',
label: <FormattedMessage id="information" defaultMessage="Information" />
},
{
value: 'configuration',
label: (
<FormattedMessage id="configuration" defaultMessage="Configuration" />
)
}
];
const theme = useTheme(); const theme = useTheme();
const [currentTab, setCurrentTab] = useState<string>('live');
const [formValues, setFormValues] = useState(props.current_installation); const [formValues, setFormValues] = useState(props.current_installation);
const requiredFields = ['name', 'region', 'location', 'country']; const requiredFields = ['name', 'region', 'location', 'country'];
const context = useContext(UserContext); const context = useContext(UserContext);
@ -100,17 +68,16 @@ function Installation(props: singleInstallationProps) {
const { installationStatus, handleLogWarningOrError, getStatus } = logContext; const { installationStatus, handleLogWarningOrError, getStatus } = logContext;
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation')); const installationId = parseInt(searchParams.get('installation'));
const currentTab = searchParams.get('tab');
const [values, setValues] = useState<TopologyValues | null>(null); const [values, setValues] = useState<TopologyValues | null>(null);
const [openModalDeleteInstallation, setOpenModalDeleteInstallation] =
useState(false);
if (formValues == undefined) { if (formValues == undefined) {
return null; return null;
} }
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value);
setError(false);
};
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormValues({ setFormValues({
@ -127,7 +94,18 @@ function Installation(props: singleInstallationProps) {
const handleDelete = (e) => { const handleDelete = (e) => {
setLoading(true); setLoading(true);
setError(false); setError(false);
setOpenModalDeleteInstallation(true);
};
const deleteInstallationModalHandle = (e) => {
setOpenModalDeleteInstallation(false);
deleteInstallation(formValues, props.type); deleteInstallation(formValues, props.type);
setLoading(false);
};
const deleteInstallationModalHandleCancel = (e) => {
setOpenModalDeleteInstallation(false);
setLoading(false);
}; };
const areRequiredFieldsFilled = () => { const areRequiredFieldsFilled = () => {
@ -155,8 +133,8 @@ function Installation(props: singleInstallationProps) {
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
setFormValues(props.current_installation); setFormValues(props.current_installation);
setErrorLoadingS3Data(false); setErrorLoadingS3Data(false);
let disconnectedStatusResult = [];
const fetchDataPeriodically = async () => { const fetchDataPeriodically = async () => {
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20)); const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
@ -165,9 +143,6 @@ function Installation(props: singleInstallationProps) {
try { try {
const res = await fetchData(now, s3Credentials); const res = await fetchData(now, s3Credentials);
if (installationId == 2) {
console.log('Fetched data from unix timestamp ' + now);
}
if (!isMounted) { if (!isMounted) {
return; return;
} }
@ -176,8 +151,22 @@ function Installation(props: singleInstallationProps) {
const newErrors: Notification[] = []; const newErrors: Notification[] = [];
if (res === FetchResult.notAvailable || res === FetchResult.tryLater) { if (res === FetchResult.notAvailable || res === FetchResult.tryLater) {
setErrorLoadingS3Data(true);
handleLogWarningOrError(props.current_installation.id, -1); handleLogWarningOrError(props.current_installation.id, -1);
disconnectedStatusResult.unshift(-1);
disconnectedStatusResult = disconnectedStatusResult.slice(0, 5);
let i = 0;
//If at least one status value shows an error, then show error
for (i; i < disconnectedStatusResult.length; i++) {
if (disconnectedStatusResult[i] != -1) {
break;
}
}
if (i === disconnectedStatusResult.length) {
setErrorLoadingS3Data(true);
}
} else { } else {
setErrorLoadingS3Data(false); setErrorLoadingS3Data(false);
setValues( setValues(
@ -237,9 +226,15 @@ function Installation(props: singleInstallationProps) {
if (newErrors.length > 0) { if (newErrors.length > 0) {
handleLogWarningOrError(props.current_installation.id, 2); handleLogWarningOrError(props.current_installation.id, 2);
disconnectedStatusResult.unshift(2);
disconnectedStatusResult = disconnectedStatusResult.slice(0, 5);
} else if (newWarnings.length > 0) { } else if (newWarnings.length > 0) {
disconnectedStatusResult.unshift(1);
disconnectedStatusResult = disconnectedStatusResult.slice(0, 5);
handleLogWarningOrError(props.current_installation.id, 1); handleLogWarningOrError(props.current_installation.id, 1);
} else { } else {
disconnectedStatusResult.unshift(0);
disconnectedStatusResult = disconnectedStatusResult.slice(0, 5);
handleLogWarningOrError(props.current_installation.id, 0); handleLogWarningOrError(props.current_installation.id, 0);
} }
} }
@ -259,291 +254,394 @@ function Installation(props: singleInstallationProps) {
if (installationId == props.current_installation.id) { if (installationId == props.current_installation.id) {
return ( return (
<Grid item xs={12} md={12}> <>
<TabsContainerWrapper> {openModalDeleteInstallation && (
<Tabs <Modal
onChange={handleTabsChange} open={openModalDeleteInstallation}
value={currentTab} onClose={() => setOpenModalDeleteInstallation(false)}
variant="scrollable" aria-labelledby="error-modal"
scrollButtons="auto" aria-describedby="error-modal-description"
textColor="primary"
indicatorColor="primary"
> >
{tabs.map((tab) => ( <Box
<Tab key={tab.value} label={tab.label} value={tab.value} /> sx={{
))} position: 'absolute',
</Tabs> top: '50%',
</TabsContainerWrapper> left: '50%',
<Card variant="outlined"> transform: 'translate(-50%, -50%)',
<Grid width: 350,
container bgcolor: 'background.paper',
direction="row" borderRadius: 4,
justifyContent="center" boxShadow: 24,
alignItems="stretch" p: 4,
spacing={0} display: 'flex',
> flexDirection: 'column',
{currentTab === 'information' && ( alignItems: 'center'
<Container maxWidth="xl"> }}
<Grid >
container <Typography
direction="row" variant="body1"
justifyContent="center" gutterBottom
alignItems="stretch" sx={{ fontWeight: 'bold' }}
spacing={3} >
Do you want to delete this installation?
</Typography>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={deleteInstallationModalHandle}
> >
<Grid item xs={12} md={12}> Delete
<CardContent> </Button>
<Box <Button
component="form" sx={{
sx={{ marginTop: 2,
'& .MuiTextField-root': { m: 1, width: '50ch' } marginLeft: 2,
}} textTransform: 'none',
noValidate bgcolor: '#ffc04d',
autoComplete="off" color: '#111111',
> '&:hover': { bgcolor: '#f7b34d' }
<div> }}
<TextField onClick={deleteInstallationModalHandleCancel}
label={ >
<FormattedMessage Cancel
id="customerName" </Button>
defaultMessage="Customer Name" </div>
/> </Box>
} </Modal>
name="name" )}
value={formValues.name}
onChange={handleChange}
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="region"
defaultMessage="Region"
/>
}
name="region"
value={formValues.region}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="location"
defaultMessage="Location"
/>
}
name="location"
value={formValues.location}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="country"
defaultMessage="Country"
/>
}
name="country"
value={formValues.country}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="orderNumbers"
defaultMessage="Order Numbers"
/>
}
name="orderNumbers"
value={formValues.orderNumbers}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
{currentUser.hasWriteAccess && ( <Grid item xs={12} md={12}>
<> <div style={{ display: 'flex', alignItems: 'center' }}>
<div> <Typography
<TextField fontWeight="bold"
label="S3 Write Key" color="text.primary"
name="s3writekey" noWrap
value={formValues.s3WriteKey} sx={{
variant="outlined" marginTop: '-20px',
fullWidth marginBottom: '10px',
/> fontSize: '14px'
</div> }}
>
<FormattedMessage
id="installation_name_simple"
defaultMessage="Installation Name:"
/>
</Typography>
<Typography
fontWeight="bold"
color="orange"
noWrap
sx={{
marginTop: '-20px',
marginBottom: '10px',
marginLeft: '5px',
fontSize: '14px'
}}
>
{props.current_installation.name}
</Typography>
</div>
<div> <Card variant="outlined">
<TextField <Grid
label="S3 Write Secret Key" container
name="s3writesecretkey" direction="row"
value={formValues.s3WriteSecret} justifyContent="center"
variant="outlined" alignItems="stretch"
fullWidth spacing={0}
/> >
</div> {currentTab === 'information' && (
<Container maxWidth="xl">
<div> <Grid
<TextField container
label="S3 Bucket Name" direction="row"
name="s3writesecretkey" justifyContent="center"
value={ alignItems="stretch"
formValues.id + spacing={3}
'-3e5b3069-214a-43ee-8d85-57d72000c19d' >
} <Grid item xs={12} md={12}>
variant="outlined" <CardContent>
fullWidth <Box
/> component="form"
</div> sx={{
</> '& .MuiTextField-root': { m: 1, width: '50ch' }
)}
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}} }}
noValidate
autoComplete="off"
> >
{currentUser.hasWriteAccess && ( <div>
<Button <TextField
variant="contained" label={
onClick={handleSubmit} <FormattedMessage
sx={{ id="customerName"
marginLeft: '10px' defaultMessage="Customer Name"
}} />
disabled={!areRequiredFieldsFilled()} }
> name="name"
<FormattedMessage value={formValues.name}
id="applyChanges" onChange={handleChange}
defaultMessage="Apply Changes" fullWidth
/> required
</Button> error={formValues.name === ''}
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleDelete}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="deleteInstallation"
defaultMessage="Delete Installation"
/>
</Button>
)}
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
/> />
)} </div>
{error && ( <div>
<Alert <TextField
severity="error" label={
sx={{ <FormattedMessage
ml: 1, id="region"
display: 'flex', defaultMessage="Region"
alignItems: 'center' />
}} }
> name="region"
<FormattedMessage value={formValues.region}
id="errorOccured" onChange={handleChange}
defaultMessage="An error has occurred" variant="outlined"
/> fullWidth
<IconButton required
color="inherit" error={formValues.name === ''}
size="small" />
onClick={() => setError(false)} </div>
sx={{ marginLeft: '4px' }} <div>
> <TextField
<CloseIcon fontSize="small" /> label={
</IconButton> <FormattedMessage
</Alert> id="location"
)} defaultMessage="Location"
{updated && ( />
<Alert }
severity="success" name="location"
sx={{ value={formValues.location}
ml: 1, onChange={handleChange}
display: 'flex', variant="outlined"
alignItems: 'center' fullWidth
}} required
> error={formValues.name === ''}
<FormattedMessage />
id="successfullyUpdated" </div>
defaultMessage="Successfully updated" <div>
/> <TextField
label={
<FormattedMessage
id="country"
defaultMessage="Country"
/>
}
name="country"
value={formValues.country}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="orderNumbers"
defaultMessage="Order Numbers"
/>
}
name="orderNumbers"
value={formValues.orderNumbers}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="installation_name"
defaultMessage="Installation Name"
/>
}
name="installationName"
value={formValues.installationName}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<IconButton {currentUser.hasWriteAccess && (
color="inherit" <>
size="small" <div>
onClick={() => setUpdated(false)} // Set error state to false on click <TextField
sx={{ marginLeft: '4px' }} label="S3 Write Key"
> name="s3writekey"
<CloseIcon fontSize="small" /> value={formValues.s3WriteKey}
</IconButton> variant="outlined"
</Alert> fullWidth
/>
</div>
<div>
<TextField
label="S3 Write Secret Key"
name="s3writesecretkey"
value={formValues.s3WriteSecret}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label="S3 Bucket Name"
name="s3writesecretkey"
value={
formValues.id +
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
}
variant="outlined"
fullWidth
/>
</div>
</>
)} )}
</div>
</Box> <div
</CardContent> style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleDelete}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="deleteInstallation"
defaultMessage="Delete Installation"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleSubmit}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
<FormattedMessage
id="applyChanges"
defaultMessage="Apply Changes"
/>
</Button>
)}
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
/>
)}
{error && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
<FormattedMessage
id="errorOccured"
defaultMessage="An error has occurred"
/>
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)}
sx={{ marginLeft: '4px' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
{updated && (
<Alert
severity="success"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
<FormattedMessage
id="successfullyUpdated"
defaultMessage="Successfully updated"
/>
<IconButton
color="inherit"
size="small"
onClick={() => setUpdated(false)} // Set error state to false on click
sx={{ marginLeft: '4px' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
</div>
</Box>
</CardContent>
</Grid>
</Grid> </Grid>
</Grid> </Container>
</Container> )}
)} {currentTab === 'overview' && (
{currentTab === 'overview' && ( <Overview s3Credentials={s3Credentials}></Overview>
<Overview s3Credentials={s3Credentials}></Overview> )}
)} {currentTab === 'configuration' && currentUser.hasWriteAccess && (
{currentTab === 'configuration' && currentUser.hasWriteAccess && ( <Configuration values={values}></Configuration>
<Configuration values={values}></Configuration> )}
)} {currentTab === 'manage' && currentUser.hasWriteAccess && (
{currentTab === 'manage' && currentUser.hasWriteAccess && ( <AccessContextProvider>
<AccessContextProvider> <Access
<Access currentResource={formValues}
currentResource={formValues} resourceType={props.type}
resourceType={props.type} ></Access>
></Access> </AccessContextProvider>
</AccessContextProvider> )}
)} {currentTab === 'live' && <Topology values={values}></Topology>}
{currentTab === 'live' && <Topology values={values}></Topology>} {currentTab === 'log' && (
{currentTab === 'log' && ( <Log
<Log warnings={warnings}
warnings={warnings} errors={errors}
errors={errors} errorLoadingS3Data={errorLoadingS3Data}
errorLoadingS3Data={errorLoadingS3Data} ></Log>
></Log> )}
)} </Grid>
</Grid> </Card>
</Card> </Grid>
</Grid> </>
); );
} else { } else {
return null; return null;

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
FormControl, FormControl,
Grid, Grid,
@ -6,34 +6,32 @@ import {
TextField, TextField,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone'; import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
import FlatInstallationView from 'src/content/dashboards/Installations/FlatInstallationView'; import FlatInstallationView from 'src/content/dashboards/Installations/FlatInstallationView';
import LogContextProvider from 'src/contexts/LogContextProvider'; import LogContextProvider from '../../../contexts/LogContextProvider';
import { I_Installation } from '../../../interfaces/InstallationTypes';
function InstallationSearch() { interface installationSearchProps {
installations: I_Installation[];
}
function InstallationSearch(props: installationSearchProps) {
const theme = useTheme(); const theme = useTheme();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const { installations, fetchAllInstallations } =
useContext(InstallationsContext);
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation')); const installationId = parseInt(searchParams.get('installation'));
useEffect(() => { const [filteredData, setFilteredData] = useState(props.installations);
fetchAllInstallations();
}, []);
const [filteredData, setFilteredData] = useState(installations);
useEffect(() => { useEffect(() => {
const filtered = installations.filter( const filtered = props.installations.filter(
(item) => (item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase()) || item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.location.toLowerCase().includes(searchTerm.toLowerCase()) item.location.toLowerCase().includes(searchTerm.toLowerCase())
); );
setFilteredData(filtered); setFilteredData(filtered);
}, [searchTerm, installations]); }, [searchTerm, props.installations]);
return ( return (
<> <>
@ -41,7 +39,7 @@ function InstallationSearch() {
<Grid <Grid
item item
xs={12} xs={12}
md={4} md={6}
sx={{ display: !installationId ? 'block' : 'none' }} sx={{ display: !installationId ? 'block' : 'none' }}
> >
<FormControl variant="outlined"> <FormControl variant="outlined">

View File

@ -1,19 +0,0 @@
import { Box, Grid, useTheme } from '@mui/material';
import InstallationsContextProvider from 'src/contexts/InstallationsContextProvider';
import InstallationSearch from './InstallationSearch';
function FlatView() {
const theme = useTheme();
return (
<InstallationsContextProvider>
<Grid item xs={12}>
<Box p={4}>
<InstallationSearch />
</Box>
</Grid>
</InstallationsContextProvider>
);
}
export default FlatView;

View File

@ -1,10 +1,9 @@
import React, { ChangeEvent, useEffect, useState } from 'react'; import React, { ChangeEvent, useContext, useEffect, useState } from 'react';
import Footer from 'src/components/Footer'; import Footer from 'src/components/Footer';
import { Card, Container, Grid, Tab, Tabs, useTheme } from '@mui/material'; import { Box, Card, Container, Grid, Tab, Tabs, useTheme } from '@mui/material';
import ListIcon from '@mui/icons-material/List'; import ListIcon from '@mui/icons-material/List';
import AccountTreeIcon from '@mui/icons-material/AccountTree'; import AccountTreeIcon from '@mui/icons-material/AccountTree';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper'; import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import UsersContextProvider from 'src/contexts/UsersContextProvider';
import { import {
Link, Link,
Route, Route,
@ -12,50 +11,220 @@ import {
useLocation, useLocation,
useNavigate useNavigate
} from 'react-router-dom'; } from 'react-router-dom';
import FlatView from './flatView';
import TreeView from '../Tree/treeView'; import TreeView from '../Tree/treeView';
import routes from 'src/Resources/routes.json'; import routes from 'src/Resources/routes.json';
import InstallationSearch from './InstallationSearch';
import { FormattedMessage } from 'react-intl';
import { UserContext } from '../../../contexts/userContext';
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
import LogContextProvider from '../../../contexts/LogContextProvider';
import Installation from './Installation';
function InstallationTabs() { function InstallationTabs() {
const theme = useTheme(); const theme = useTheme();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const tabs = [
{ const searchParams = new URLSearchParams(location.search);
value: 'list', const installationId = parseInt(searchParams.get('installation'));
label: 'Flat view', const [singleInstallationID, setSingleInstallationID] = useState(-1);
icon: <ListIcon id="mode-toggle-button-list-icon" /> const context = useContext(UserContext);
}, const { currentUser, setUser } = context;
{
value: 'tree',
label: 'Tree view',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
}
];
const [currentTab, setCurrentTab] = useState<string>('list'); const [currentTab, setCurrentTab] = useState<string>('list');
const { installations, fetchAllInstallations } =
useContext(InstallationsContext);
useEffect(() => { useEffect(() => {
//console.log(location.pathname); if (installations.length === 0) {
if ( fetchAllInstallations();
location.pathname === '/installations' || }
location.pathname === '/installations/'
) { if (installations.length === 1) {
navigate(routes.installations + routes.list, { navigate(`list?installation=${installations[0].id}&tab=live`, {
replace: true replace: true
}); });
} else if (location.pathname === '/installations/tree') { setCurrentTab('live');
setCurrentTab('tree'); } else {
if (
location.pathname === '/installations' ||
location.pathname === '/installations/'
) {
navigate(routes.installations + routes.list, {
replace: true
});
} else if (location.pathname === '/installations/tree/') {
setCurrentTab('tree');
} else if (location.pathname === '/installations/list/') {
setCurrentTab('list');
}
if (installationId) {
navigate(`?installation=${installationId}&tab=live`, {
replace: true
});
setCurrentTab('live');
}
} }
}, [location.pathname, navigate]); }, [location.pathname, navigate, installationId, installations]);
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => { const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value); setCurrentTab(value);
navigate(value);
}; };
return ( const singleInstallationTabs = currentUser.hasWriteAccess
<UsersContextProvider> ? [
<Container maxWidth="xl" sx={{ marginTop: '20px' }}> {
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
,
{
value: 'manage',
label: (
<FormattedMessage id="manage" defaultMessage="Access Management" />
)
},
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
},
{
value: 'configuration',
label: (
<FormattedMessage
id="configuration"
defaultMessage="Configuration"
/>
)
}
]
: [
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
,
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
}
];
const tabs = installationId
? currentUser.hasWriteAccess
? [
{
value: 'list',
icon: <ListIcon id="mode-toggle-button-list-icon" />
},
{
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
},
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
,
{
value: 'manage',
label: (
<FormattedMessage
id="manage"
defaultMessage="Access Management"
/>
)
},
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
},
{
value: 'configuration',
label: (
<FormattedMessage
id="configuration"
defaultMessage="Configuration"
/>
)
}
]
: [
{
value: 'list',
icon: <ListIcon id="mode-toggle-button-list-icon" />
},
{
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
},
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
,
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
}
]
: [
{
value: 'list',
icon: <ListIcon id="mode-toggle-button-list-icon" />
},
{
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
}
];
return installations.length > 1 ? (
<>
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
<TabsContainerWrapper> <TabsContainerWrapper>
<Tabs <Tabs
onChange={handleTabsChange} onChange={handleTabsChange}
@ -71,7 +240,12 @@ function InstallationTabs() {
value={tab.value} value={tab.value}
icon={tab.icon} icon={tab.icon}
component={Link} component={Link}
to={routes[tab.value]} label={tab.label}
to={
tab.value === 'list' || tab.value === 'tree'
? routes[tab.value]
: `?installation=${installationId}&tab=${routes[tab.value]}`
}
/> />
))} ))}
</Tabs> </Tabs>
@ -85,15 +259,79 @@ function InstallationTabs() {
spacing={0} spacing={0}
> >
<Routes> <Routes>
<Route path={routes.list + '*'} element={<FlatView />} /> <Route
path={routes.list + '*'}
element={
<Grid item xs={12}>
<Box p={4}>
<InstallationSearch installations={installations} />
</Box>
</Grid>
}
/>
<Route path={routes.tree + '*'} element={<TreeView />} /> <Route path={routes.tree + '*'} element={<TreeView />} />
</Routes> </Routes>
</Grid> </Grid>
</Card> </Card>
</Container> </Container>
<Footer /> <Footer />
</UsersContextProvider> </>
); ) : installations.length === 1 ? (
<>
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
<TabsContainerWrapper>
<Tabs
onChange={handleTabsChange}
value={currentTab}
variant="scrollable"
scrollButtons="auto"
textColor="primary"
indicatorColor="primary"
>
{singleInstallationTabs.map((tab) => (
<Tab
key={tab.value}
value={tab.value}
component={Link}
label={tab.label}
to={`?installation=${installations[0].id}&tab=${
routes[tab.value]
}`}
/>
))}
</Tabs>
</TabsContainerWrapper>
<Card variant="outlined">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={0}
>
<Routes>
<Route
path={routes.list + '*'}
element={
<Grid item xs={12}>
<Box p={4}>
<LogContextProvider>
<Installation
current_installation={installations[0]}
type="installation"
></Installation>
</LogContextProvider>
</Box>
</Grid>
}
/>
</Routes>
</Grid>
</Card>
</Container>
<Footer />
</>
) : null;
} }
export default InstallationTabs; export default InstallationTabs;

View File

@ -66,6 +66,8 @@ function installationForm(props: installationFormProps) {
return true; return true;
}; };
const isMobile = window.innerWidth <= 1490;
return ( return (
<> <>
<Modal <Modal
@ -77,10 +79,10 @@ function installationForm(props: installationFormProps) {
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: '30%', top: isMobile ? '50%' : '30%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: 600, width: 500,
bgcolor: 'background.paper', bgcolor: 'background.paper',
borderRadius: 4, borderRadius: 4,
boxShadow: 24, boxShadow: 24,

View File

@ -34,10 +34,10 @@ function Log(props: LogProps) {
<Grid container> <Grid container>
<Grid item xs={12} md={12}> <Grid item xs={12} md={12}>
{(props.errors.length > 0 || props.warnings.length > 0) && ( {(props.errors.length > 0 || props.warnings.length > 0) && (
<Card> <Card sx={{ marginTop: '10px' }}>
<Divider /> <Divider />
<TableContainer> <TableContainer>
<Table sx={{ height: 10 }}> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell> <TableCell>
@ -82,7 +82,7 @@ function Log(props: LogProps) {
color="text.primary" color="text.primary"
gutterBottom gutterBottom
noWrap noWrap
sx={{ marginTop: '10px' }} sx={{ marginTop: '5px' }}
> >
{error.device} {error.device}
</Typography> </Typography>
@ -94,7 +94,7 @@ function Log(props: LogProps) {
color="text.primary" color="text.primary"
gutterBottom gutterBottom
noWrap noWrap
sx={{ marginTop: '10px' }} sx={{ marginTop: '5px' }}
> >
{error.description} {error.description}
</Typography> </Typography>
@ -106,7 +106,7 @@ function Log(props: LogProps) {
color="text.primary" color="text.primary"
gutterBottom gutterBottom
noWrap noWrap
sx={{ marginTop: '10px' }} sx={{ marginTop: '5px' }}
> >
{error.date} {error.date}
</Typography> </Typography>
@ -118,7 +118,7 @@ function Log(props: LogProps) {
color="text.primary" color="text.primary"
gutterBottom gutterBottom
noWrap noWrap
sx={{ marginTop: '10px' }} sx={{ marginTop: '5px' }}
> >
{error.time} {error.time}
</Typography> </Typography>
@ -202,11 +202,9 @@ function Log(props: LogProps) {
<Alert <Alert
severity="error" severity="error"
sx={{ sx={{
ml: 1,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
marginTop: '20px', marginTop: '20px'
marginBottom: '20px'
}} }}
> >
<FormattedMessage <FormattedMessage
@ -227,7 +225,6 @@ function Log(props: LogProps) {
<Alert <Alert
severity="error" severity="error"
sx={{ sx={{
ml: 1,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
marginTop: '20px' marginTop: '20px'
@ -249,10 +246,10 @@ function Log(props: LogProps) {
<Alert <Alert
severity="error" severity="error"
sx={{ sx={{
ml: 1,
display: 'flex', display: 'flex',
alignItems: 'center' alignItems: 'center',
//marginBottom: '20px' //marginBottom: '20px'
marginTop: '20px'
}} }}
> >
<FormattedMessage <FormattedMessage

View File

@ -35,6 +35,15 @@ export type BoxData = {
}; };
export type TopologyValues = { export type TopologyValues = {
gridBox: BoxData;
pvOnAcGridBox: BoxData;
loadOnAcGridBox: BoxData;
pvOnIslandBusBox: BoxData;
loadOnIslandBusBox: BoxData;
pvOnDcBox: BoxData;
loadOnDcBox: BoxData;
batteryBox: BoxData;
grid: BoxData; grid: BoxData;
gridToAcInConnection: BoxData; gridToAcInConnection: BoxData;
gridBus: BoxData; gridBus: BoxData;
@ -64,6 +73,15 @@ export type TopologyValues = {
type TopologyPaths = { [key in keyof TopologyValues]: string[] }; type TopologyPaths = { [key in keyof TopologyValues]: string[] };
export const topologyPaths: TopologyPaths = { export const topologyPaths: TopologyPaths = {
gridBox: ['/Config/Devices/GridMeterIp/DeviceState'],
pvOnAcGridBox: ['/Config/Devices/PvOnAcGrid/DeviceState'],
loadOnAcGridBox: ['/Config/Devices/LoadOnAcGrid/DeviceState'],
pvOnIslandBusBox: ['/Config/Devices/PvOnAcIsland/DeviceState'],
loadOnIslandBusBox: ['/Config/Devices/IslandBusLoadMeterIp/DeviceState'],
pvOnDcBox: ['/Config/Devices/PvOnDc/DeviceState'],
loadOnDcBox: ['/Config/Devices/LoadOnDc/DeviceState'],
batteryBox: ['/Config/Devices/BatteryIp/DeviceState'],
grid: [ grid: [
'/GridMeter/Ac/L1/Power/Active', '/GridMeter/Ac/L1/Power/Active',
'/GridMeter/Ac/L2/Power/Active', '/GridMeter/Ac/L2/Power/Active',
@ -86,7 +104,7 @@ export const topologyPaths: TopologyPaths = {
'/AcDc/Ac/L2/Power/Active', '/AcDc/Ac/L2/Power/Active',
'/AcDc/Ac/L3/Power/Active' '/AcDc/Ac/L3/Power/Active'
], ],
islandBusToLoadOnIslandBusConnection: ['/LoadOnAcIsland/Power/Active'], islandBusToLoadOnIslandBusConnection: ['/LoadOnAcIsland/Ac/Power/Active'],
islandBusToInverter: ['/AcDc/Dc/Power'], islandBusToInverter: ['/AcDc/Dc/Power'],
pvOnIslandBusToIslandBusConnection: ['/PvOnAcIsland/Power/Active'], pvOnIslandBusToIslandBusConnection: ['/PvOnAcIsland/Power/Active'],
@ -104,8 +122,8 @@ export const topologyPaths: TopologyPaths = {
dcBusToLoadOnDcConnection: ['/LoadOnDc/Power'], dcBusToLoadOnDcConnection: ['/LoadOnDc/Power'],
dcDc: ['/DcDc/Dc/Battery/Voltage'], dcDc: ['/DcDc/Dc/Battery/Voltage'],
dcDCToBatteryConnection: ['/Battery/Dc/Power'],
dcDCToBatteryConnection: ['/DcDc/Dc/Link/Power'],
battery: [ battery: [
'/Battery/Soc', '/Battery/Soc',
'/Battery/Dc/Voltage', '/Battery/Dc/Voltage',
@ -124,7 +142,7 @@ export const topologyPaths: TopologyPaths = {
], ],
minimumSoC: ['/Config/MinSoc'], minimumSoC: ['/Config/MinSoc'],
installedDcDcPower: ['/DcDc/SystemControl/TargetSlave'], installedDcDcPower: ['/DcDc/SystemControl/NumberOfConnectedSlaves'],
gridSetPoint: ['/Config/GridSetPoint'], gridSetPoint: ['/Config/GridSetPoint'],
maximumDischargePower: ['/Config/MaxBatteryDischargingCurrent'], maximumDischargePower: ['/Config/MaxBatteryDischargingCurrent'],
calibrationChargeForced: ['/Config/ForceCalibrationCharge'] calibrationChargeForced: ['/Config/ForceCalibrationCharge']
@ -138,14 +156,10 @@ export const extractValues = (
for (const topologyKey of Object.keys(topologyPaths)) { for (const topologyKey of Object.keys(topologyPaths)) {
const paths = topologyPaths[topologyKey]; const paths = topologyPaths[topologyKey];
let topologyValues: { unit: string; value: string | number }[] = []; let topologyValues: { unit: string; value: string | number }[] = [];
//console.log('paths is ', paths);
// Check if any of the specified paths exist in the dataRecord // Check if any of the specified paths exist in the dataRecord
for (const path of paths) { for (const path of paths) {
//console.log(' path is ', path);
if (timeSeriesData.value.hasOwnProperty(path)) { if (timeSeriesData.value.hasOwnProperty(path)) {
//console.log('matching path is ', path);
//console.log(timeSeriesData.value[path]);
topologyValues.push({ topologyValues.push({
unit: timeSeriesData.value[path].unit, unit: timeSeriesData.value[path].unit,
value: timeSeriesData.value[path].value value: timeSeriesData.value[path].value
@ -179,7 +193,19 @@ export const getAmount = (
highestConnectionValue: number, highestConnectionValue: number,
values: I_BoxDataValue[] values: I_BoxDataValue[]
) => { ) => {
return Math.abs(values[0].value as number) / highestConnectionValue; // console.log(
// 'value=',
// Math.abs(values[0].value as number),
// 'highest is',
// highestConnectionValue,
// 'and amount is ',
// parseFloat(
// (Math.abs(values[0].value as number) / highestConnectionValue).toFixed(1)
// )
// );
return parseFloat(
(Math.abs(values[0].value as number) / highestConnectionValue).toFixed(1)
);
}; };
export const createTimes = ( export const createTimes = (
@ -187,9 +213,13 @@ export const createTimes = (
numberOfNodes: number numberOfNodes: number
): UnixTime[] => { ): UnixTime[] => {
const oneSpan = range.duration.divide(numberOfNodes); const oneSpan = range.duration.divide(numberOfNodes);
//console.log(oneSpan);
const roundedRange = TimeRange.fromTimes( const roundedRange = TimeRange.fromTimes(
range.start.round(oneSpan), range.start.round(oneSpan),
range.end.round(oneSpan) range.end.round(oneSpan)
); );
return roundedRange.sample(oneSpan);
const unixTimes = range.sample(oneSpan);
return unixTimes;
}; };

View File

@ -2,21 +2,45 @@
import { ApexOptions } from 'apexcharts'; import { ApexOptions } from 'apexcharts';
import { chartInfoInterface } from 'src/interfaces/Chart'; import { chartInfoInterface } from 'src/interfaces/Chart';
import { findPower, formatPowerForGraph } from '../../../Resources/formatPower'; import { findPower, formatPowerForGraph } from 'src/Resources/formatPower';
import { addHours, format } from 'date-fns';
export const getChartOptions = (chartInfo: chartInfoInterface): ApexOptions => { export const getChartOptions = (chartInfo: chartInfoInterface): ApexOptions => {
// Custom datetime formatter for GMT+2
const customDatetimeFormatter = (timestamp, options) => {
const gmtDate = new Date(timestamp); // Convert Unix timestamp to milliseconds
const gmtPlus2Date = addHours(gmtDate, 4); // Add 2 hours to convert to GMT+2
// Use the specified options to format the date and time
const year = format(gmtDate, 'yyyy');
const month = format(gmtDate, "MMM 'yy");
const day = format(gmtDate, 'dd MMM');
const hour = format(gmtDate, 'HH:mm');
const minute = format(gmtDate, 'mm');
// Return the formatted date and time based on the provided options
return ` ${hour}:${minute}`;
};
const chartOptions: ApexOptions = { const chartOptions: ApexOptions = {
chart: { chart: {
id: 'area-datetime', id: 'area-datetime',
toolbar: {
show: false
},
type: 'area', type: 'area',
height: 350, height: 350,
zoom: { zoom: {
autoScaleYaxis: false autoScaleYaxis: false
} }
}, },
// markers: {
// size: 1,
// strokeColors: 'black'
// },
dataLabels: { dataLabels: {
enabled: false enabled: false
}, },
fill: { fill: {
type: 'gradient', type: 'gradient',
gradient: { gradient: {
@ -36,7 +60,8 @@ export const getChartOptions = (chartInfo: chartInfoInterface): ApexOptions => {
year: 'yyyy', year: 'yyyy',
month: "MMM 'yy", month: "MMM 'yy",
day: 'dd MMM', day: 'dd MMM',
hour: 'HH:mm' hour: 'HH:mm',
minute: 'mm'
} }
} }
}, },
@ -45,11 +70,19 @@ export const getChartOptions = (chartInfo: chartInfoInterface): ApexOptions => {
width: 2 width: 2
}, },
yaxis: { yaxis: {
min: chartInfo.min > 0 ? 0 : undefined, min:
chartInfo.min >= 0
? 0
: chartInfo.max <= 0
? Math.ceil(chartInfo.min / findPower(chartInfo.min).value) *
findPower(chartInfo.min).value
: undefined,
max: max:
chartInfo.min > 0 chartInfo.min >= 0
? Math.round(chartInfo.max / findPower(chartInfo.max).value) * ? Math.ceil(chartInfo.max / findPower(chartInfo.max).value) *
findPower(chartInfo.max).value findPower(chartInfo.max).value
: chartInfo.max <= 0
? 0
: undefined, : undefined,
title: { title: {
text: chartInfo.unit, text: chartInfo.unit,
@ -61,30 +94,26 @@ export const getChartOptions = (chartInfo: chartInfoInterface): ApexOptions => {
rotate: 0 rotate: 0
}, },
labels: { labels: {
formatter: formatter: function (value: number) {
chartInfo.min > 0 return formatPowerForGraph(
? function (value: number) { value,
return formatPowerForGraph( Math.max(Math.abs(chartInfo.max), Math.abs(chartInfo.min))
value, ).value.toString();
chartInfo.max }
).value.toString();
}
: function (value: number) {
return formatPowerForGraph(
value,
chartInfo.max
).value.toString();
}
} }
}, },
tooltip: { tooltip: {
x: { x: {
format: 'dd MMM HH:mm' format: 'dd MMM HH:mm:ss'
}, },
y: { y: {
formatter: function (val, opts) { formatter: function (val, opts) {
return ( return (
formatPowerForGraph(val, chartInfo.max).value.toFixed(2) + formatPowerForGraph(
val,
Math.max(Math.abs(chartInfo.max), Math.abs(chartInfo.min))
).value.toFixed(2) +
' ' + ' ' +
chartInfo.unit chartInfo.unit
); );

View File

@ -7,16 +7,24 @@ import {
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import ReactApexChart from 'react-apexcharts'; import ReactApexChart from 'react-apexcharts';
import { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import DataCache from 'src/dataCache/dataCache'; import DataCache from 'src/dataCache/dataCache';
import { TimeSpan, UnixTime } from 'src/dataCache/time'; import { TimeRange, TimeSpan, UnixTime } from 'src/dataCache/time';
import { BehaviorSubject, startWith, throttleTime, withLatestFrom } from 'rxjs'; import {
BehaviorSubject,
combineLatest,
startWith,
tap,
throttleTime
} from 'rxjs';
import { RecordSeries } from 'src/dataCache/data'; import { RecordSeries } from 'src/dataCache/data';
import { createTimes } from '../Log/graph.util'; import { createTimes } from '../Log/graph.util';
import { I_S3Credentials } from 'src/interfaces/S3Types'; import { I_S3Credentials } from 'src/interfaces/S3Types';
import { getChartOptions } from './chartOptions'; import { getChartOptions } from './chartOptions';
import { chartDataInterface, overviewInterface } from 'src/interfaces/Chart'; import { chartDataInterface, overviewInterface } from 'src/interfaces/Chart';
import { fetchData } from 'src/content/dashboards/Installations/fetchData'; import { fetchData } from 'src/content/dashboards/Installations/fetchData';
import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl';
const prefixes = ['', 'k', 'M', 'G', 'T']; const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999; const MAX_NUMBER = 9999999;
@ -27,9 +35,12 @@ interface OverviewProps {
function Overview(props: OverviewProps) { function Overview(props: OverviewProps) {
const theme = useTheme(); const theme = useTheme();
const timeRange = createTimes( const numOfPointsToFetch = 100;
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)), const [timeRange, setTimeRange] = useState(
200 createTimes(
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)),
numOfPointsToFetch
)
); );
const [chartData, setChartData] = useState<chartDataInterface>({ const [chartData, setChartData] = useState<chartDataInterface>({
@ -102,38 +113,49 @@ function Overview(props: OverviewProps) {
magnitude: 0, magnitude: 0,
unit: '', unit: '',
min: MAX_NUMBER, min: MAX_NUMBER,
max: 0 max: -MAX_NUMBER
}; };
}); });
console.log(input);
input.forEach((item) => { input.forEach((item) => {
const csvContent = item.value; const csvContent = item.value;
pathsToSearch.forEach((path) => { pathsToSearch.forEach((path) => {
if (csvContent && csvContent[path]) { if (csvContent) {
const timestamp = item.time.ticks * 1000; const timestamp = item.time.ticks * 1000;
const value = csvContent[path];
// const result: { magnitude: number; value: number } = formatPower(
// value.value
// );
if (value.value < overviewData[path].min) {
overviewData[path].min = value.value;
}
if (value.value > overviewData[path].max) { const adjustedTimestamp = new Date(timestamp);
overviewData[path].max = value.value; adjustedTimestamp.setHours(adjustedTimestamp.getHours() + 2);
}
// if (result.magnitude > result.magnitude) { if (csvContent[path]) {
// overviewData[path].magnitude = result.magnitude; const value = csvContent[path];
// }
data[path].push([timestamp, value.value]); if (value.value < overviewData[path].min) {
overviewData[path].min = value.value;
}
if (value.value > overviewData[path].max) {
overviewData[path].max = value.value;
}
data[path].push([adjustedTimestamp, value.value]);
} else {
//data[path].push([adjustedTimestamp, null]);
}
} else {
// data[path].push([
// addHours(new Date(item.time.ticks * 1000), 2),
// null
// ]);
} }
}); });
}); });
pathsToSearch.forEach((path) => { pathsToSearch.forEach((path) => {
let value = overviewData[path].max; let value = Math.max(
Math.abs(overviewData[path].max),
Math.abs(overviewData[path].min)
);
let negative = false; let negative = false;
let magnitude = 0; let magnitude = 0;
@ -145,8 +167,6 @@ function Overview(props: OverviewProps) {
value /= 1000; value /= 1000;
magnitude++; magnitude++;
} }
console.log(path, magnitude);
overviewData[path].magnitude = prefixes[magnitude]; overviewData[path].magnitude = prefixes[magnitude];
}); });
@ -175,8 +195,6 @@ function Overview(props: OverviewProps) {
path = '/Battery/Dc/Power'; path = '/Battery/Dc/Power';
chartData.dcPower = [{ name: 'Battery Power', data: data[path] }]; chartData.dcPower = [{ name: 'Battery Power', data: data[path] }];
//console.log(overviewData[path]);
//console.log(data[path]);
chartOverview.dcPower = { chartOverview.dcPower = {
magnitude: overviewData[path].magnitude, magnitude: overviewData[path].magnitude,
unit: '(' + overviewData[path].magnitude + 'W' + ')', unit: '(' + overviewData[path].magnitude + 'W' + ')',
@ -218,37 +236,129 @@ function Overview(props: OverviewProps) {
chartData: chartData, chartData: chartData,
chartOverview: chartOverview chartOverview: chartOverview
}; };
//return chartData;
}; };
useEffect(() => { useEffect(() => {
const subscription = cache.gotData const left = cache.gotData.pipe(
.pipe( startWith(UnixTime.fromTicks(0)),
startWith(0), throttleTime(200, undefined, { leading: true, trailing: true })
throttleTime(200, undefined, { leading: true, trailing: true }), );
withLatestFrom(times$)
)
.subscribe(([_, times]) => {
const timeSeries = cache.getSeries(times);
console.log(timeSeries); const right = times$.pipe(
tap((times) => {
//console.log(times);
})
);
const result: { const combined = combineLatest([left, right]);
chartData: chartDataInterface;
chartOverview: overviewInterface;
} = transformToGraphData(timeSeries);
setChartData(result.chartData); const subscription = combined.subscribe(([_, times]) => {
setChartOverview(result.chartOverview); const timeSeries = cache.getSeries(times);
}); const result: {
chartData: chartDataInterface;
chartOverview: overviewInterface;
} = transformToGraphData(timeSeries);
setChartData(result.chartData);
setChartOverview(result.chartOverview);
});
return () => subscription.unsubscribe(); return () => subscription.unsubscribe();
}, []); }, []);
const handleBeforeZoom = (chartContext, { xaxis }) => {
const startX = parseInt(xaxis.min) / 1000;
const endX = parseInt(xaxis.max) / 1000;
const times = createTimes(
TimeRange.fromTimes(UnixTime.fromTicks(startX), UnixTime.fromTicks(endX)),
numOfPointsToFetch
);
cache.getSeries(times);
times$.next(times);
};
const handleDoubleClick = () => {
const times = createTimes(
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)),
numOfPointsToFetch
);
cache.getSeries(times);
times$.next(times);
};
const handle24HourData = () => {
const times = createTimes(
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)),
numOfPointsToFetch
);
cache.getSeries(times);
times$.next(times);
};
const handleWeekData = () => {
const times = createTimes(
UnixTime.now().rangeBefore(TimeSpan.fromWeeks(1)),
numOfPointsToFetch
);
cache.getSeries(times);
times$.next(times);
};
const handleMonthData = () => {
const times = createTimes(
UnixTime.now().rangeBefore(TimeSpan.fromWeeks(4)),
numOfPointsToFetch
);
cache.getSeries(times);
times$.next(times);
};
const renderGraphs = () => { const renderGraphs = () => {
return ( return (
<Container maxWidth="xl"> <Container maxWidth="xl">
<Grid container> <Grid container>
<Grid item xs={12} md={12}>
<Button
variant="contained"
onClick={handle24HourData}
sx={{
marginTop: '20px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="24_hours" defaultMessage="24-hours" />
</Button>
<Button
variant="contained"
onClick={handleWeekData}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="lastweek" defaultMessage="Last week" />
</Button>
<Button
variant="contained"
onClick={handleMonthData}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="lastmonth" defaultMessage="Last Month" />
</Button>
</Grid>
<Grid item xs={12} md={12}> <Grid item xs={12} md={12}>
<Grid <Grid
container container
@ -273,7 +383,10 @@ function Overview(props: OverviewProps) {
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box> <Box>
<Typography variant="subtitle1" noWrap> <Typography variant="subtitle1" noWrap>
Battery SOC (State Of Charge) <FormattedMessage
id="battery_soc"
defaultMessage="Battery SOC (State Of Charge)"
/>
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@ -287,7 +400,14 @@ function Overview(props: OverviewProps) {
></Box> ></Box>
</Box> </Box>
<ReactApexChart <ReactApexChart
options={getChartOptions(chartOverview.soc)} options={{
...getChartOptions(chartOverview.soc),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.soc} series={chartData.soc}
type="area" type="area"
height={350} height={350}
@ -310,7 +430,10 @@ function Overview(props: OverviewProps) {
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box> <Box>
<Typography variant="subtitle1" noWrap> <Typography variant="subtitle1" noWrap>
Battery Temperature <FormattedMessage
id="battery_temperature"
defaultMessage="Battery Temperature"
/>
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@ -323,12 +446,21 @@ function Overview(props: OverviewProps) {
}} }}
></Box> ></Box>
</Box> </Box>
<ReactApexChart <div onDoubleClick={handleDoubleClick}>
options={getChartOptions(chartOverview.temperature)} <ReactApexChart
series={chartData.temperature} options={{
type="line" ...getChartOptions(chartOverview.temperature),
height={350} chart: {
/> events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.temperature}
type="area"
height={350}
/>
</div>
</Card> </Card>
</Grid> </Grid>
</Grid> </Grid>
@ -356,7 +488,10 @@ function Overview(props: OverviewProps) {
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box> <Box>
<Typography variant="subtitle1" noWrap> <Typography variant="subtitle1" noWrap>
Battery Power <FormattedMessage
id="pv_production"
defaultMessage="PV Production"
/>
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@ -369,12 +504,21 @@ function Overview(props: OverviewProps) {
}} }}
></Box> ></Box>
</Box> </Box>
<ReactApexChart <div onDoubleClick={handleDoubleClick}>
options={getChartOptions(chartOverview.dcPower)} <ReactApexChart
series={chartData.dcPower} options={{
type="area" ...getChartOptions(chartOverview.pvProduction),
height={350} chart: {
/> events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.pvProduction}
type="area"
height={350}
/>
</div>
</Card> </Card>
</Grid> </Grid>
<Grid item md={6} xs={12}> <Grid item md={6} xs={12}>
@ -393,7 +537,10 @@ function Overview(props: OverviewProps) {
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box> <Box>
<Typography variant="subtitle1" noWrap> <Typography variant="subtitle1" noWrap>
Grid Power <FormattedMessage
id="grid_power"
defaultMessage="Grid Power"
/>
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@ -406,12 +553,21 @@ function Overview(props: OverviewProps) {
}} }}
></Box> ></Box>
</Box> </Box>
<ReactApexChart <div onDoubleClick={handleDoubleClick}>
options={getChartOptions(chartOverview.gridPower)} <ReactApexChart
series={chartData.gridPower} options={{
type="area" ...getChartOptions(chartOverview.gridPower),
height={350} chart: {
/> events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.gridPower}
type="area"
height={350}
/>
</div>
</Card> </Card>
</Grid> </Grid>
</Grid> </Grid>
@ -438,7 +594,10 @@ function Overview(props: OverviewProps) {
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box> <Box>
<Typography variant="subtitle1" noWrap> <Typography variant="subtitle1" noWrap>
PV Production <FormattedMessage
id="battery_power"
defaultMessage="Battery Power"
/>
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@ -451,12 +610,21 @@ function Overview(props: OverviewProps) {
}} }}
></Box> ></Box>
</Box> </Box>
<ReactApexChart <div onDoubleClick={handleDoubleClick}>
options={getChartOptions(chartOverview.pvProduction)} <ReactApexChart
series={chartData.pvProduction} options={{
type="area" ...getChartOptions(chartOverview.dcPower),
height={350} chart: {
/> events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.dcPower}
type="area"
height={350}
/>
</div>
</Card> </Card>
</Grid> </Grid>
<Grid item md={6} xs={12}> <Grid item md={6} xs={12}>
@ -475,7 +643,10 @@ function Overview(props: OverviewProps) {
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box> <Box>
<Typography variant="subtitle1" noWrap> <Typography variant="subtitle1" noWrap>
DC Bus Voltage <FormattedMessage
id="dc_voltage"
defaultMessage="DC Bus Voltage"
/>
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@ -488,12 +659,21 @@ function Overview(props: OverviewProps) {
}} }}
></Box> ></Box>
</Box> </Box>
<ReactApexChart <div onDoubleClick={handleDoubleClick}>
options={getChartOptions(chartOverview.dcBusVoltage)} <ReactApexChart
series={chartData.dcBusVoltage} options={{
type="line" ...getChartOptions(chartOverview.dcBusVoltage),
height={350} chart: {
/> events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.dcBusVoltage}
type="area"
height={350}
/>
</div>
</Card> </Card>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -23,6 +23,8 @@ function Topology(props: TopologyProps) {
setShowValues(!showValues); setShowValues(!showValues);
}; };
const isMobile = window.innerWidth <= 1490;
return ( return (
<Container maxWidth="xl" style={{ backgroundColor: 'white' }}> <Container maxWidth="xl" style={{ backgroundColor: 'white' }}>
<Grid container> <Grid container>
@ -56,7 +58,7 @@ function Topology(props: TopologyProps) {
xs={12} xs={12}
md={12} md={12}
style={{ style={{
height: '600px', height: isMobile ? '550px' : '600px',
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@ -66,7 +68,9 @@ function Topology(props: TopologyProps) {
<TopologyColumn <TopologyColumn
centerBox={{ centerBox={{
title: 'Grid', title: 'Grid',
data: props.values.grid data: props.values.grid,
connected:
props.values.gridBox.values[0].value.toString() != 'Disabled'
}} }}
centerConnection={{ centerConnection={{
orientation: 'horizontal', orientation: 'horizontal',
@ -85,7 +89,10 @@ function Topology(props: TopologyProps) {
<TopologyColumn <TopologyColumn
topBox={{ topBox={{
title: 'Pv Inverter', title: 'Pv Inverter',
data: props.values.gridBusToPvOnGridbusConnection data: props.values.gridBusToPvOnGridbusConnection,
connected:
props.values.pvOnAcGridBox.values[0].value.toString() !=
'Disabled'
}} }}
topConnection={{ topConnection={{
orientation: 'vertical', orientation: 'vertical',
@ -101,22 +108,27 @@ function Topology(props: TopologyProps) {
}} }}
centerBox={{ centerBox={{
title: 'Grid Bus', title: 'Grid Bus',
data: props.values.gridBus data: props.values.gridBus,
connected: true
}} }}
centerConnection={{ centerConnection={{
orientation: 'horizontal', orientation: 'horizontal',
data: props.values.gridBusToIslandBusConnection,
amount: props.values.gridBusToIslandBusConnection amount: props.values.gridBusToIslandBusConnection
? getAmount( ? getAmount(
highestConnectionValue, highestConnectionValue,
props.values.gridBusToIslandBusConnection.values props.values.gridBusToIslandBusConnection.values
) )
: 0, : 0,
data: props.values.gridBusToIslandBusConnection,
showValues: showValues showValues: showValues
}} }}
bottomBox={{ bottomBox={{
title: 'AC Loads', title: 'AC Loads',
data: props.values.gridBusToLoadOnGridBusConnection data: props.values.gridBusToLoadOnGridBusConnection,
connected:
props.values.loadOnAcGridBox.values[0].value.toString() !=
'Disabled'
}} }}
bottomConnection={{ bottomConnection={{
orientation: 'vertical', orientation: 'vertical',
@ -136,7 +148,10 @@ function Topology(props: TopologyProps) {
<TopologyColumn <TopologyColumn
topBox={{ topBox={{
title: 'Pv Inverter', title: 'Pv Inverter',
data: props.values.pvOnIslandBusToIslandBusConnection data: props.values.pvOnIslandBusToIslandBusConnection,
connected:
props.values.pvOnIslandBusBox.values[0].value.toString() !=
'Disabled'
}} }}
topConnection={{ topConnection={{
orientation: 'vertical', orientation: 'vertical',
@ -152,7 +167,8 @@ function Topology(props: TopologyProps) {
}} }}
centerBox={{ centerBox={{
title: 'Island Bus', title: 'Island Bus',
data: props.values.islandBus data: props.values.islandBus,
connected: true
}} }}
centerConnection={{ centerConnection={{
orientation: 'horizontal', orientation: 'horizontal',
@ -167,7 +183,10 @@ function Topology(props: TopologyProps) {
}} }}
bottomBox={{ bottomBox={{
title: 'AC Loads', title: 'AC Loads',
data: props.values.islandBusToLoadOnIslandBusConnection data: props.values.islandBusToLoadOnIslandBusConnection,
connected:
props.values.loadOnIslandBusBox.values[0].value.toString() !=
'Disabled'
}} }}
bottomConnection={{ bottomConnection={{
orientation: 'vertical', orientation: 'vertical',
@ -187,7 +206,8 @@ function Topology(props: TopologyProps) {
<TopologyColumn <TopologyColumn
centerBox={{ centerBox={{
title: 'AC-DC', title: 'AC-DC',
data: props.values.inverter data: props.values.inverter,
connected: true
}} }}
centerConnection={{ centerConnection={{
orientation: 'horizontal', orientation: 'horizontal',
@ -205,8 +225,10 @@ function Topology(props: TopologyProps) {
/> />
<TopologyColumn <TopologyColumn
topBox={{ topBox={{
title: 'Pv DcDc', title: 'Pv DC-DC',
data: props.values.pvOnDcBusToDcBusConnection data: props.values.pvOnDcBusToDcBusConnection,
connected:
props.values.pvOnDcBox.values[0].value.toString() != 'Disabled'
}} }}
topConnection={{ topConnection={{
orientation: 'vertical', orientation: 'vertical',
@ -222,7 +244,8 @@ function Topology(props: TopologyProps) {
}} }}
centerBox={{ centerBox={{
title: 'DC Link', title: 'DC Link',
data: props.values.dcBus data: props.values.dcBus,
connected: true
}} }}
centerConnection={{ centerConnection={{
orientation: 'horizontal', orientation: 'horizontal',
@ -237,7 +260,10 @@ function Topology(props: TopologyProps) {
}} }}
bottomBox={{ bottomBox={{
title: 'DC Loads', title: 'DC Loads',
data: props.values.dcBusToLoadOnDcConnection data: props.values.dcBusToLoadOnDcConnection,
connected:
props.values.loadOnDcBox.values[0].value.toString() !=
'Disabled'
}} }}
bottomConnection={{ bottomConnection={{
orientation: 'vertical', orientation: 'vertical',
@ -257,7 +283,8 @@ function Topology(props: TopologyProps) {
<TopologyColumn <TopologyColumn
centerBox={{ centerBox={{
title: 'DC-DC', title: 'DC-DC',
data: props.values.dcDc data: props.values.dcDc,
connected: true
}} }}
centerConnection={{ centerConnection={{
orientation: 'horizontal', orientation: 'horizontal',
@ -276,7 +303,9 @@ function Topology(props: TopologyProps) {
<TopologyColumn <TopologyColumn
centerBox={{ centerBox={{
title: 'Battery', title: 'Battery',
data: props.values.battery data: props.values.battery,
connected:
props.values.batteryBox.values[0].value.toString() != 'Disabled'
}} }}
isLast={true} isLast={true}
isFirst={false} isFirst={false}

View File

@ -5,6 +5,17 @@
border-bottom: none; border-bottom: none;
} }
.isMobile.horizontalLine {
position: absolute;
overflow: hidden;
border-left: none;
margin-left: 142px;
border-right: none;
}
.horizontalLine { .horizontalLine {
position: absolute; position: absolute;
overflow: hidden; overflow: hidden;
@ -15,59 +26,60 @@
.dotRight { .dotRight {
position: absolute; position: absolute;
margin-left: 35px;
width: 3px; width: 3px;
height: 3px; height: 3px;
border-radius: 50%; border-radius: 50%;
background-color: #f7b34d; background-color: #f7b34d;
animation: rightflow 2s linear infinite; animation: rightflow 2s linear infinite;
transform: translateX(-39px); /* Initial position off-screen */
} }
.dotLeft { .dotLeft {
position: absolute; position: absolute;
margin-left: 35px;
width: 3px; width: 3px;
height: 3px; height: 3px;
border-radius: 50%; border-radius: 50%;
background-color: #f7b34d; background-color: #f7b34d;
animation: leftflow 2s linear infinite; animation: leftflow 2s linear infinite;
transform: translateX(-39px); /* Initial position off-screen */
} }
.verticalDotDown { .verticalDotDown {
position: absolute; position: absolute;
margin-top: 35px;
width: 3px; width: 3px;
height: 3px; height: 3px;
border-radius: 50%; border-radius: 50%;
background-color: #f7b34d; background-color: #f7b34d;
animation: verticalDownFlow 2s linear infinite; animation: verticalDownFlow 2s linear infinite;
transform: translateY(-39px);
} }
@keyframes rightflow { @keyframes rightflow {
0% { 0% {
left: -35px; transform: translateX(-35px);
} }
100% { 100% {
left: 110%; transform: translateX(1000%);
} }
} }
@keyframes leftflow { @keyframes leftflow {
0% { 0% {
left: 110px; transform: translateX(1000%);
} }
100% { 100% {
left: -35px; transform: translateX(-35px);
} }
} }
@keyframes verticalDownFlow { @keyframes verticalDownFlow {
0% { 0% {
top: -35px; transform: translateY(-35px);
} }
100% { 100% {
top: 100%; transform: translateY(1050%);
} }
} }

View File

@ -12,6 +12,7 @@ import BatteryCharging60Icon from '@mui/icons-material/BatteryCharging60';
import OutletIcon from '@mui/icons-material/Outlet'; import OutletIcon from '@mui/icons-material/Outlet';
import SolarPowerIcon from '@mui/icons-material/SolarPower'; import SolarPowerIcon from '@mui/icons-material/SolarPower';
import PowerInputIcon from '@mui/icons-material/PowerInput'; import PowerInputIcon from '@mui/icons-material/PowerInput';
import SignalWifiConnectedNoInternet4Icon from '@mui/icons-material/SignalWifiConnectedNoInternet4';
import BoltIcon from '@mui/icons-material/Bolt'; import BoltIcon from '@mui/icons-material/Bolt';
import { BoxData } from '../Log/graph.util'; import { BoxData } from '../Log/graph.util';
import inverterImage from 'src/Resources/images/inverter.png'; import inverterImage from 'src/Resources/images/inverter.png';
@ -21,6 +22,7 @@ import converterImage from 'src/Resources/images/converter.png';
export interface TopologyBoxProps { export interface TopologyBoxProps {
title: string; title: string;
data?: BoxData; data?: BoxData;
connected?: boolean;
} }
const isInt = (value: number) => { const isInt = (value: number) => {
@ -45,7 +47,7 @@ function formatPower(value) {
magnitude++; magnitude++;
} }
const roundedValue = value.toFixed(2); const roundedValue = value.toFixed(1);
return negative === false return negative === false
? `${roundedValue} ${prefixes[magnitude]}` ? `${roundedValue} ${prefixes[magnitude]}`
@ -54,28 +56,32 @@ function formatPower(value) {
function TopologyBox(props: TopologyBoxProps) { function TopologyBox(props: TopologyBoxProps) {
const theme = useTheme(); const theme = useTheme();
const isMobile = window.innerWidth <= 1490;
return ( return (
<Card <Card
sx={{ sx={{
visibility: visibility:
props.data && props.data.values[0].value != 0 ? 'visible' : 'hidden', //props.data && props.data.values[0].value != 0 ? 'visible' : 'hidden',
width: '104px', props.connected ? 'visible' : 'hidden',
width: isMobile ? '90px' : '104px',
height: height:
props.title === 'Battery' props.title === 'Battery'
? '165px' ? '165px'
: props.title === 'AC Loads' || : props.title === 'AC Loads' ||
props.title === 'DC Loads' || props.title === 'DC Loads' ||
props.title === 'Pv Inverter' || props.title === 'Pv Inverter' ||
props.title === 'Pv DcDc' props.title === 'Pv DC-DC'
? '100px' ? '100px'
: '150px', : '150px',
backgroundColor: backgroundColor: !props.data
props.title === 'Grid Bus' || ? 'darkgrey'
props.title === 'Island Bus' || : props.title === 'Grid Bus' ||
props.title === 'DC Link' props.title === 'Island Bus' ||
? '#f7b34d' props.title === 'DC Link'
: 'none', ? '#f7b34d'
: 'none',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
@ -93,7 +99,7 @@ function TopologyBox(props: TopologyBoxProps) {
textAlign: 'center' textAlign: 'center'
}} }}
> >
{props.title === 'Battery' && ( {props.data && props.title === 'Battery' && (
<Typography variant="h5" noWrap> <Typography variant="h5" noWrap>
{props.title} (x{props.data.values.length - 4}) {props.title} (x{props.data.values.length - 4})
</Typography> </Typography>
@ -104,7 +110,20 @@ function TopologyBox(props: TopologyBoxProps) {
</Typography> </Typography>
)} )}
{props.title === 'AC-DC' && ( {!props.data && (
<SignalWifiConnectedNoInternet4Icon
style={{
fontSize: 40,
color: 'black',
backgroundColor: 'darkgrey',
padding: '5px',
borderTopLeftRadius: '4px',
borderTopRightRadius: '4px'
}}
></SignalWifiConnectedNoInternet4Icon>
)}
{props.data && props.title === 'AC-DC' && (
<img <img
src={inverterImage} src={inverterImage}
style={{ style={{
@ -115,7 +134,7 @@ function TopologyBox(props: TopologyBoxProps) {
/> />
)} )}
{props.title === 'DC Link' && ( {props.data && props.title === 'DC Link' && (
<PowerInputIcon <PowerInputIcon
style={{ style={{
fontSize: 40, fontSize: 40,
@ -128,7 +147,7 @@ function TopologyBox(props: TopologyBoxProps) {
></PowerInputIcon> ></PowerInputIcon>
)} )}
{props.title === 'DC-DC' && ( {props.data && props.title === 'DC-DC' && (
<img <img
src={converterImage} src={converterImage}
style={{ style={{
@ -139,18 +158,20 @@ function TopologyBox(props: TopologyBoxProps) {
/> />
)} )}
{(props.title === 'Grid Bus' || props.title === 'Island Bus') && ( {props.data &&
<img (props.title === 'Grid Bus' || props.title === 'Island Bus') && (
src={acCurrentImage} <img
style={{ src={acCurrentImage}
width: '40px', style={{
height: '40px', width: '40px',
backgroundColor: '#f7b34d' height: '40px',
}} backgroundColor: '#f7b34d'
/> }}
)} />
)}
{props.title != 'AC-DC' && {props.data &&
props.title != 'AC-DC' &&
props.title != 'DC Link' && props.title != 'DC Link' &&
props.title != 'DC-DC' && ( props.title != 'DC-DC' && (
<AvatarWrapper <AvatarWrapper
@ -163,7 +184,7 @@ function TopologyBox(props: TopologyBoxProps) {
}} }}
> >
{(props.title === 'Pv Inverter' || {(props.title === 'Pv Inverter' ||
props.title === 'Pv DcDc') && ( props.title === 'Pv DC-DC') && (
<SolarPowerIcon <SolarPowerIcon
style={{ style={{
fontSize: 30, fontSize: 30,
@ -212,6 +233,7 @@ function TopologyBox(props: TopologyBoxProps) {
)} )}
{props.data && <Divider sx={{ width: '150%', marginTop: '0px' }} />} {props.data && <Divider sx={{ width: '150%', marginTop: '0px' }} />}
{props.data && ( {props.data && (
<Box <Box
sx={{ sx={{

View File

@ -16,6 +16,8 @@ interface TopologyColumnProps {
} }
function TopologyColumn(props: TopologyColumnProps) { function TopologyColumn(props: TopologyColumnProps) {
const isMobile = window.innerWidth <= 1490;
return ( return (
<Box <Box
sx={{ sx={{
@ -23,7 +25,7 @@ function TopologyColumn(props: TopologyColumnProps) {
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginLeft: props.isFirst ? '0px' : '68px' marginLeft: props.isFirst ? '0px' : isMobile ? '52px' : '68px'
}} }}
> >
<TopologyBox {...props.topBox}></TopologyBox> <TopologyBox {...props.topBox}></TopologyBox>

View File

@ -45,7 +45,7 @@ function formatPower(value) {
magnitude++; magnitude++;
} }
const roundedValue = value.toFixed(2); const roundedValue = value.toFixed(1);
return negative === false return negative === false
? `${roundedValue} ${prefixes[magnitude]}` ? `${roundedValue} ${prefixes[magnitude]}`
@ -61,12 +61,14 @@ function TopologyFlow(props: TopologyFlowProps) {
{ animationDelay: string; left: string }[] { animationDelay: string; left: string }[]
>([]); >([]);
const numOfDots = 400; const numOfDots = 200;
const minNumberOfDots = 100; const minNumberOfDots = 50;
const minHeight = 2; const minHeight = 2;
const maxHeight = 70; const maxHeight = 70;
const minWidth = 2; const minWidth = 2;
const maxWidth = 68; const maxWidth = 68;
const maxWidthMobile = 50;
const isMobile = window.innerWidth <= 1490;
const desiredNumOfDots = const desiredNumOfDots =
numOfDots * props.amount < minNumberOfDots numOfDots * props.amount < minNumberOfDots
@ -83,7 +85,7 @@ function TopologyFlow(props: TopologyFlowProps) {
setDotStyleVertical( setDotStyleVertical(
Array.from({ length: desiredNumOfDots }, () => getRandomStyleVertical()) Array.from({ length: desiredNumOfDots }, () => getRandomStyleVertical())
); );
}, []); }, [desiredNumOfDots]);
return ( return (
<> <>
@ -91,7 +93,7 @@ function TopologyFlow(props: TopologyFlowProps) {
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
marginLeft: '170px', marginLeft: isMobile ? '142px' : '170px',
marginTop: '2px', marginTop: '2px',
backgroundColor: 'transparent', backgroundColor: 'transparent',
display: 'flex', display: 'flex',
@ -103,6 +105,7 @@ function TopologyFlow(props: TopologyFlowProps) {
{props.showValues && props.data.values[0].value != 0 && ( {props.showValues && props.data.values[0].value != 0 && (
<Typography <Typography
sx={{ sx={{
marginTop: '1px',
color: 'black', color: 'black',
bgcolor: 'background.paper', bgcolor: 'background.paper',
borderRadius: 5, borderRadius: 5,
@ -116,16 +119,16 @@ function TopologyFlow(props: TopologyFlowProps) {
)} )}
</Box> </Box>
)} )}
<div <div
className={props.orientation === 'vertical' ? 'line' : 'horizontalLine'} className={`${
props.orientation === 'vertical' ? 'line' : 'horizontalLine'
} ${isMobile ? 'isMobile' : ''}`}
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
position: position:
props.orientation === 'horizontal' ? 'absolute' : 'relative', props.orientation === 'horizontal' ? 'absolute' : 'relative',
height: height:
props.orientation === 'horizontal' props.orientation === 'horizontal'
? `${Math.min( ? `${Math.min(
@ -133,13 +136,14 @@ function TopologyFlow(props: TopologyFlowProps) {
maxHeight maxHeight
)}px` )}px`
: `${maxHeight}px`, : `${maxHeight}px`,
width: width:
props.orientation === 'vertical' props.orientation === 'vertical'
? `${Math.min( ? `${Math.min(
Math.max(props.amount * maxWidth, minWidth), Math.max(props.amount * maxWidth, minWidth),
maxWidth maxWidth
)}px` )}px`
: isMobile
? `${maxWidthMobile}px`
: `${maxWidth}px` : `${maxWidth}px`
}} }}
> >

View File

@ -110,7 +110,7 @@ function CustomTreeItem(props: CustomTreeItemProps) {
height: '23px', height: '23px',
color: 'red', color: 'red',
borderRadius: '50%', borderRadius: '50%',
marginLeft: '21px', marginLeft: '30px',
marginTop: '30px' marginTop: '30px'
}} }}
/> />
@ -123,7 +123,7 @@ function CustomTreeItem(props: CustomTreeItemProps) {
size={20} size={20}
sx={{ sx={{
color: '#f7b34d', color: '#f7b34d',
marginLeft: '20px', marginLeft: '22px',
marginTop: '30px' marginTop: '30px'
}} }}
/> />
@ -136,7 +136,7 @@ function CustomTreeItem(props: CustomTreeItemProps) {
width: '20px', width: '20px',
height: '20px', height: '20px',
borderRadius: '50%', borderRadius: '50%',
marginLeft: '20px', marginLeft: '17px',
backgroundColor: backgroundColor:
status === 2 status === 2
? 'red' ? 'red'

View File

@ -8,9 +8,11 @@ import {
Container, Container,
Grid, Grid,
IconButton, IconButton,
Modal,
Tab, Tab,
Tabs, Tabs,
TextField, TextField,
Typography,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material'; import { Close as CloseIcon } from '@mui/icons-material';
@ -46,6 +48,7 @@ function Folder(props: singleFolderProps) {
const selectedBulkActions = selectedUser !== -1; const selectedBulkActions = selectedUser !== -1;
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
const folderId = parseInt(searchParams.get('folder')); const folderId = parseInt(searchParams.get('folder'));
const [openModalDeleteFolder, setOpenModalDeleteFolder] = useState(false);
const installationContext = useContext(InstallationsContext); const installationContext = useContext(InstallationsContext);
const { const {
@ -126,7 +129,18 @@ function Folder(props: singleFolderProps) {
const handleDeleteFolder = (e) => { const handleDeleteFolder = (e) => {
setLoading(true); setLoading(true);
setError(false); setError(false);
setOpenModalDeleteFolder(true);
};
const deleteFolderModalHandle = (e) => {
setOpenModalDeleteFolder(false);
deleteFolder(formValues); deleteFolder(formValues);
setLoading(false);
};
const deleteFolderModalHandleCancel = (e) => {
setOpenModalDeleteFolder(false);
setLoading(false);
}; };
const handleFolderFormSubmit = () => { const handleFolderFormSubmit = () => {
@ -155,6 +169,80 @@ function Folder(props: singleFolderProps) {
if (folderId == props.current_folder.id) { if (folderId == props.current_folder.id) {
return ( return (
<> <>
{openModalDeleteFolder && (
<Modal
open={openModalDeleteFolder}
onClose={() => setOpenModalDeleteFolder(false)}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 350,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography
variant="body1"
gutterBottom
sx={{ fontWeight: 'bold' }}
>
Do you want to delete this folder?
</Typography>
<Typography
variant="body1"
gutterBottom
sx={{ fontSize: '0.875rem' }}
>
All installations of this folder will be deleted.
</Typography>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={deleteFolderModalHandle}
>
Delete
</Button>
<Button
sx={{
marginTop: 2,
marginLeft: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={deleteFolderModalHandleCancel}
>
Cancel
</Button>
</div>
</Box>
</Modal>
)}
{openModalFolder && ( {openModalFolder && (
<FolderForm <FolderForm
cancel={handleFormCancel} cancel={handleFormCancel}
@ -169,7 +257,7 @@ function Folder(props: singleFolderProps) {
parentid={props.current_folder.id} parentid={props.current_folder.id}
/> />
)} )}
<Grid item xs={12} md={9}> <Grid item xs={12} md={12}>
<TabsContainerWrapper> <TabsContainerWrapper>
<Tabs <Tabs
onChange={handleTabsChange} onChange={handleTabsChange}
@ -253,30 +341,14 @@ function Folder(props: singleFolderProps) {
{currentUser.hasWriteAccess && ( {currentUser.hasWriteAccess && (
<Button <Button
variant="contained" variant="contained"
onClick={handleFolderInformationUpdate} onClick={handleDeleteFolder}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
<FormattedMessage
id="applyChanges"
defaultMessage="Apply Changes"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleNewInstallationInsertion}
sx={{ sx={{
marginLeft: '10px' marginLeft: '10px'
}} }}
> >
<FormattedMessage <FormattedMessage
id="addNewInstallation" id="deleteFolder"
defaultMessage="Add new installation" defaultMessage="Delete Folder"
/> />
</Button> </Button>
)} )}
@ -299,14 +371,30 @@ function Folder(props: singleFolderProps) {
{currentUser.hasWriteAccess && ( {currentUser.hasWriteAccess && (
<Button <Button
variant="contained" variant="contained"
onClick={handleDeleteFolder} onClick={handleNewInstallationInsertion}
sx={{ sx={{
marginLeft: '10px' marginLeft: '10px'
}} }}
> >
<FormattedMessage <FormattedMessage
id="deleteFolder" id="addNewInstallation"
defaultMessage="Delete Folder" defaultMessage="Add new installation"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleFolderInformationUpdate}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
<FormattedMessage
id="applyChanges"
defaultMessage="Apply Changes"
/> />
</Button> </Button>
)} )}

View File

@ -7,7 +7,6 @@ import CustomTreeItem from './CustomTreeItem';
import Installation from '../Installations/Installation'; import Installation from '../Installations/Installation';
import Folder from './Folder'; import Folder from './Folder';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import LogContextProvider from 'src/contexts/LogContextProvider';
function InstallationTree() { function InstallationTree() {
const { foldersAndInstallations, fetchAllFoldersAndInstallations } = const { foldersAndInstallations, fetchAllFoldersAndInstallations } =
@ -47,46 +46,44 @@ function InstallationTree() {
}; };
return ( return (
<LogContextProvider> <Grid container spacing={1} sx={{ marginTop: 0.1 }}>
<Grid container spacing={1} sx={{ marginTop: 0.1 }}> <Grid item xs={12} md={12}>
<Grid item xs={12} md={3}> <TreeView
<TreeView defaultCollapseIcon={<ExpandMoreIcon />}
defaultCollapseIcon={<ExpandMoreIcon />} defaultExpandIcon={<ChevronRightIcon />}
defaultExpandIcon={<ChevronRightIcon />} defaultExpanded={['1Folder']}
defaultExpanded={['1Folder']} >
> {foldersAndInstallations.map((node, index) => {
{foldersAndInstallations.map((node, index) => {
return (
<TreeNode
key={node.id.toString() + node.type}
node={node}
parent_id={'0'}
/>
);
})}
</TreeView>
</Grid>
{foldersAndInstallations.map((installation) => {
if (installation.type == 'Installation') {
return ( return (
<Installation <TreeNode
key={installation.id + installation.type} key={node.id.toString() + node.type}
current_installation={installation} node={node}
type="tree" parent_id={'0'}
></Installation> />
); );
} else { })}
return ( </TreeView>
<Folder
key={installation.id + installation.type}
current_folder={installation}
></Folder>
);
}
})}
</Grid> </Grid>
</LogContextProvider>
{foldersAndInstallations.map((installation) => {
if (installation.type == 'Installation') {
return (
<Installation
key={installation.id + installation.type}
current_installation={installation}
type="tree"
></Installation>
);
} else {
return (
<Folder
key={installation.id + installation.type}
current_folder={installation}
></Folder>
);
}
})}
</Grid>
); );
} }

View File

@ -55,6 +55,8 @@ function folderForm(props: folderFormProps) {
props.cancel(); props.cancel();
}; };
const isMobile = window.innerWidth <= 1490;
const areRequiredFieldsFilled = () => { const areRequiredFieldsFilled = () => {
for (const field of requiredFields) { for (const field of requiredFields) {
if (!formValues[field]) { if (!formValues[field]) {
@ -75,7 +77,7 @@ function folderForm(props: folderFormProps) {
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: '30%', top: isMobile ? '50%' : '30%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: 600, width: 600,

View File

@ -1,6 +1,7 @@
import { Box, Grid, useTheme } from '@mui/material'; import { Box, Grid, useTheme } from '@mui/material';
import InstallationTree from './InstallationTree'; import InstallationTree from './InstallationTree';
import InstallationsContextProvider from 'src/contexts/InstallationsContextProvider'; import InstallationsContextProvider from 'src/contexts/InstallationsContextProvider';
import LogContextProvider from '../../../contexts/LogContextProvider';
function TreeView() { function TreeView() {
const theme = useTheme(); const theme = useTheme();
@ -9,7 +10,9 @@ function TreeView() {
<InstallationsContextProvider> <InstallationsContextProvider>
<Grid item xs={12}> <Grid item xs={12}>
<Box p={4}> <Box p={4}>
<InstallationTree /> <LogContextProvider>
<InstallationTree />
</LogContextProvider>
</Box> </Box>
</Grid> </Grid>
</InstallationsContextProvider> </InstallationsContextProvider>

View File

@ -14,6 +14,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import { InnovEnergyUser } from 'src/interfaces/UserTypes'; import { InnovEnergyUser } from 'src/interfaces/UserTypes';
import User from './User'; import User from './User';
import { useNavigate } from 'react-router-dom';
interface FlatUsersViewProps { interface FlatUsersViewProps {
users: InnovEnergyUser[]; users: InnovEnergyUser[];
@ -23,10 +24,14 @@ interface FlatUsersViewProps {
const FlatUsersView = (props: FlatUsersViewProps) => { const FlatUsersView = (props: FlatUsersViewProps) => {
const [selectedUser, setSelectedUser] = useState<number>(-1); const [selectedUser, setSelectedUser] = useState<number>(-1);
const selectedBulkActions = selectedUser !== -1; const selectedBulkActions = selectedUser !== -1;
const navigate = useNavigate();
const handleSelectOneUser = (installationID: number): void => { const handleSelectOneUser = (installationID: number): void => {
if (selectedUser != installationID) { if (selectedUser != installationID) {
setSelectedUser(installationID); setSelectedUser(installationID);
// navigate(routes.users + '?user=' + installationID.toString(), {
// replace: true
// });
} else { } else {
setSelectedUser(-1); setSelectedUser(-1);
} }
@ -46,12 +51,14 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
return props.users.find((user) => user.id === id); return props.users.find((user) => user.id === id);
}; };
const isMobile = window.innerWidth <= 1490;
return ( return (
<Grid container spacing={1} sx={{ marginTop: '1px' }}> <Grid container spacing={1} sx={{ marginTop: '1px' }}>
<Grid item xs={12} md={3}> <Grid item xs={12} md={isMobile ? 5 : 4}>
<Card> <Card>
<Divider /> <Divider />
<TableContainer> <TableContainer sx={{ maxHeight: '520px', overflowY: 'auto' }}>
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
@ -67,7 +74,7 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
isRowHovered === user.id isRowHovered === user.id
? { ? {
cursor: 'pointer', cursor: 'pointer',
backgroundColor: theme.colors.primary.lighter // Set your desired hover background color here backgroundColor: theme.colors.primary.lighter
} }
: {}; : {};
@ -113,7 +120,7 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
</Card> </Card>
</Grid> </Grid>
{selectedBulkActions && ( {selectedUser && (
<User <User
current_user={findUser(selectedUser)} current_user={findUser(selectedUser)}
fetchDataAgain={props.fetchDataAgain} fetchDataAgain={props.fetchDataAgain}

View File

@ -8,9 +8,11 @@ import {
Container, Container,
Grid, Grid,
IconButton, IconButton,
Modal,
Tab, Tab,
Tabs, Tabs,
TextField, TextField,
Typography,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material'; // Import CloseIcon import { Close as CloseIcon } from '@mui/icons-material'; // Import CloseIcon
@ -19,6 +21,7 @@ import axiosConfig from 'src/Resources/axiosConfig';
import { InnovEnergyUser } from 'src/interfaces/UserTypes'; import { InnovEnergyUser } from 'src/interfaces/UserTypes';
import { TokenContext } from 'src/contexts/tokenContext'; import { TokenContext } from 'src/contexts/tokenContext';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper'; import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import { FormattedMessage } from 'react-intl';
interface singleUserProps { interface singleUserProps {
current_user: InnovEnergyUser; current_user: InnovEnergyUser;
@ -34,6 +37,8 @@ function User(props: singleUserProps) {
const [formValues, setFormValues] = useState(props.current_user); const [formValues, setFormValues] = useState(props.current_user);
const tokencontext = useContext(TokenContext); const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext; const { removeToken } = tokencontext;
const tabs = [{ value: 'user', label: 'User' }];
const [openModalDeleteFolder, setOpenModalDeleteFolder] = useState(false);
useEffect(() => { useEffect(() => {
setFormValues(props.current_user); setFormValues(props.current_user);
@ -43,8 +48,6 @@ function User(props: singleUserProps) {
return null; return null;
} }
const tabs = [{ value: 'user', label: 'User' }];
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => { const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value); setCurrentTab(value);
setError(false); setError(false);
@ -60,32 +63,21 @@ function User(props: singleUserProps) {
const handleSubmit = (e) => { const handleSubmit = (e) => {
setLoading(true); setLoading(true);
setError(false); setError(false);
axiosConfig
.put('/UpdateUser', formValues)
.then((response) => {
if (response) {
props.fetchDataAgain();
setLoading(false);
setUpdated(true);
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
}; };
const handleDelete = (e) => { const handleDelete = (e) => {
setLoading(true); setLoading(true);
setError(false); setError(false);
setOpenModalDeleteFolder(true);
};
const deleteUserModalHandleCancel = (e) => {
setLoading(false);
setError(false);
setOpenModalDeleteFolder(false);
};
const deleteUserModalHandle = (e) => {
axiosConfig axiosConfig
.delete(`/DeleteUser?userId=${formValues.id}`) .delete(`/DeleteUser?userId=${formValues.id}`)
.then((response) => { .then((response) => {
@ -102,15 +94,86 @@ function User(props: singleUserProps) {
.catch((error) => { .catch((error) => {
setLoading(false); setLoading(false);
setError(true); setError(true);
setOpenModalDeleteFolder(false);
if (error.response && error.response.status == 401) { if (error.response && error.response.status == 401) {
removeToken(); removeToken();
} }
}); });
}; };
const isMobile = window.innerWidth <= 1490;
return ( return (
<> <>
<Grid item xs={12} md={9}> {openModalDeleteFolder && (
<Modal
open={openModalDeleteFolder}
onClose={() => setOpenModalDeleteFolder(false)}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 350,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography
variant="body1"
gutterBottom
sx={{ fontWeight: 'bold' }}
>
Do you want to delete this user?
</Typography>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={deleteUserModalHandle}
>
Delete
</Button>
<Button
sx={{
marginTop: 2,
marginLeft: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={deleteUserModalHandleCancel}
>
Cancel
</Button>
</div>
</Box>
</Modal>
)}
<Grid item xs={12} md={isMobile ? 7 : 8}>
<TabsContainerWrapper> <TabsContainerWrapper>
<Tabs <Tabs
onChange={handleTabsChange} onChange={handleTabsChange}
@ -195,7 +258,10 @@ function User(props: singleUserProps) {
marginLeft: '10px' marginLeft: '10px'
}} }}
> >
Apply Changes <FormattedMessage
id="apply_changes"
defaultMessage="Apply Changes"
/>
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
@ -204,7 +270,10 @@ function User(props: singleUserProps) {
marginLeft: '10px' marginLeft: '10px'
}} }}
> >
Delete User <FormattedMessage
id="delete_user"
defaultMessage="Delete User"
/>
</Button> </Button>
{loading && ( {loading && (

View File

@ -50,6 +50,8 @@ function UsersSearch() {
setOpenModal(false); setOpenModal(false);
}; };
const isMobile = window.innerWidth <= 1490;
return ( return (
<> <>
<Grid container spacing={1}> <Grid container spacing={1}>
@ -65,7 +67,7 @@ function UsersSearch() {
<UserForm cancel={handleUserFormCancel} submit={handleUserFormSubmit} /> <UserForm cancel={handleUserFormCancel} submit={handleUserFormSubmit} />
)} )}
<Grid container spacing={1} sx={{ marginTop: '1px' }}> <Grid container spacing={1} sx={{ marginTop: '1px' }}>
<Grid item xs={12} md={3}> <Grid item xs={12} md={isMobile ? 5 : 3}>
<FormControl variant="outlined" fullWidth> <FormControl variant="outlined" fullWidth>
<TextField <TextField
placeholder="Search" placeholder="Search"

View File

@ -2,6 +2,7 @@ import Footer from 'src/components/Footer';
import { Box, Container, Grid, useTheme } from '@mui/material'; import { Box, Container, Grid, useTheme } from '@mui/material';
import UsersSearch from './UsersSearch'; import UsersSearch from './UsersSearch';
import UsersContextProvider from 'src/contexts/UsersContextProvider'; import UsersContextProvider from 'src/contexts/UsersContextProvider';
import React from 'react';
function Users() { function Users() {
const theme = useTheme(); const theme = useTheme();

View File

@ -103,6 +103,8 @@ function userForm(props: userFormProps) {
setSelectedInstallationNames(event.target.value); setSelectedInstallationNames(event.target.value);
}; };
const isMobile = window.innerWidth <= 1490;
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
const res = await axiosConfig.post('/CreateUser', { const res = await axiosConfig.post('/CreateUser', {
...formValues, ...formValues,
@ -189,10 +191,10 @@ function userForm(props: userFormProps) {
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: '30%', top: isMobile ? '50%' : '30%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: 600, width: 500,
bgcolor: 'background.paper', bgcolor: 'background.paper',
borderRadius: 4, borderRadius: 4,
boxShadow: 24, boxShadow: 24,

View File

@ -1,4 +1,4 @@
import { AxiosError } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { import {
createContext, createContext,
ReactNode, ReactNode,
@ -54,7 +54,7 @@ const InstallationsContextProvider = ({
}: { }: {
children: ReactNode; children: ReactNode;
}) => { }) => {
const [installations, setInstallations] = useState([]); const [installations, setInstallations] = useState<I_Installation[]>([]);
const [foldersAndInstallations, setFoldersAndInstallations] = useState([]); const [foldersAndInstallations, setFoldersAndInstallations] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
@ -67,7 +67,7 @@ const InstallationsContextProvider = ({
let isMounted = true; let isMounted = true;
axiosConfig axiosConfig
.get('/GetAllInstallations', {}) .get('/GetAllInstallations', {})
.then((res) => { .then((res: AxiosResponse<I_Installation[]>) => {
setInstallations(res.data); setInstallations(res.data);
}) })
.catch((err: AxiosError) => { .catch((err: AxiosError) => {

View File

@ -16,9 +16,9 @@ export const LogContextProvider = ({ children }: { children: ReactNode }) => {
>({}); >({});
const BUFFER_LENGTH = 5; const BUFFER_LENGTH = 5;
const handleLogWarningOrError = (installation_id: number, value: number) => { const handleLogWarningOrError = (installation_id: number, value: number) => {
setInstallationStatus((prevStatus) => { setInstallationStatus((prevStatus) => {
// Create a new object by spreading the previous state
const newStatus = { ...prevStatus }; const newStatus = { ...prevStatus };
if (!newStatus.hasOwnProperty(installation_id)) { if (!newStatus.hasOwnProperty(installation_id)) {
@ -29,6 +29,7 @@ export const LogContextProvider = ({ children }: { children: ReactNode }) => {
0, 0,
BUFFER_LENGTH BUFFER_LENGTH
); );
return newStatus; return newStatus;
}); });
}; };
@ -39,9 +40,19 @@ export const LogContextProvider = ({ children }: { children: ReactNode }) => {
if (!installationStatus.hasOwnProperty(installationId)) { if (!installationStatus.hasOwnProperty(installationId)) {
status = -2; status = -2;
} else { } else {
if (installationId === 2) { let i = 0;
console.log(installationStatus[2]); //If at least one status value shows an error, then show error
for (i; i < installationStatus[installationId].length; i++) {
if (installationStatus[installationId][i] === 2) {
status = 2;
return status;
}
if (installationStatus[installationId][i] === 1) {
status = 1;
return status;
}
} }
if (installationStatus[installationId][0] == -1) { if (installationStatus[installationId][0] == -1) {
let i = 0; let i = 0;
for (i; i < installationStatus[installationId].length; i++) { for (i; i < installationStatus[installationId].length; i++) {
@ -49,7 +60,6 @@ export const LogContextProvider = ({ children }: { children: ReactNode }) => {
break; break;
} }
} }
if (i === installationStatus[installationId].length) { if (i === installationStatus[installationId].length) {
status = -1; status = -1;
} }
@ -57,7 +67,6 @@ export const LogContextProvider = ({ children }: { children: ReactNode }) => {
status = installationStatus[installationId][0]; status = installationStatus[installationId][0];
} }
} }
return status; return status;
}; };

View File

@ -1,9 +1,5 @@
import {createContext, ReactNode, useState} from 'react'; import {createContext, ReactNode, useState} from 'react';
//const setUser=(currentUser: InnovEnergyUser)=>{
// localStorage.setItem("currentUser",JSON.stringify(currentUser));
//}
// Define the shape of the context // Define the shape of the context
interface TokenContextType { interface TokenContextType {
token?: string | null; token?: string | null;
@ -18,6 +14,8 @@ export const TokenContext = createContext<TokenContextType | undefined>(
// Create a UserContextProvider component // Create a UserContextProvider component
export const TokenContextProvider = ({ children }: { children: ReactNode }) => { export const TokenContextProvider = ({ children }: { children: ReactNode }) => {
const searchParams = new URLSearchParams(location.search);
const tokenId = parseInt(searchParams.get('authToken'));
//Initialize context state with a "null" user //Initialize context state with a "null" user
const [token, setToken] = useState(localStorage.getItem('token')); const [token, setToken] = useState(localStorage.getItem('token'));

View File

@ -265,6 +265,7 @@ export class TimeRange {
for (let t = this.from; t < this.to; t += period.ticks) for (let t = this.from; t < this.to; t += period.ticks)
samples.push(UnixTime.fromTicks(t)); samples.push(UnixTime.fromTicks(t));
samples.push(UnixTime.fromTicks(this.to));
return samples; return samples;
} }

View File

@ -9,7 +9,8 @@ export interface I_Installation extends I_S3Credentials {
location: string; location: string;
region: string; region: string;
country: string; country: string;
orderNumbers: string; installationName: string;
orderNumbers: string[] | string;
lat: number; lat: number;
long: number; long: number;
id: number; id: number;

View File

@ -2,12 +2,12 @@
"information": "Information", "information": "Information",
"addNewChild": "Neues Kind hinzufügen", "addNewChild": "Neues Kind hinzufügen",
"addNewDialogButton": "Neue Dialogschaltfläche hinzufügen", "addNewDialogButton": "Neue Dialogschaltfläche hinzufügen",
"addUser": "Nutzer erstellen", "addUser": "Neuen Benutzer erstellen",
"alarms": "Alarme", "alarms": "Alarme",
"applyChanges": "Änderungen speichern", "applyChanges": "Änderungen speichern",
"country": "Land", "country": "Land",
"createNewFolder": "Neuen Ordner erstellen", "createNewFolder": "Neuer Ordner",
"createNewUser": "Neuen Nutzer erstellen", "createNewUser": "Neuer Benutzer",
"customerName": "Kundenname", "customerName": "Kundenname",
"email": "Email", "email": "Email",
"english": "Englisch", "english": "Englisch",
@ -16,27 +16,50 @@
"german": "Deutsch", "german": "Deutsch",
"groupTabs": "Gruppen", "groupTabs": "Gruppen",
"groupTree": "Gruppenbaum", "groupTree": "Gruppenbaum",
"overview": "Überblick",
"manage": "Zugriffsverwaltung",
"configuration": "Aufbau",
"installation_name_simple": "Installationsname: ",
"language": "Sprache",
"minimum_soc": "Minimum SoC",
"calibration_charge_forced": "Kalibrierungsladung erzwungen",
"grid_set_point": "Sollwert Netzleistung",
"Installed_Power_DC1010": "Installierte Leistung DC1010 TODO",
"Maximum_Discharge_Power": "Maximale Entladeleistung",
"Number_of_Batteries": "Anzahl der Batterien",
"24_hours": "24 Stunden",
"lastweek": "Letzte Woche",
"lastmonth": "Vergangener Monat",
"apply_changes": "Änderungen übernehmen",
"delete_user": "Benutzer löschen",
"battery_temperature": "Batterietemperatur",
"pv_production": "Photovoltaik Produktion",
"grid_power": "Netzstrom",
"battery_power": "Batterieleistung",
"dc_voltage": "DC-Busspannung",
"battery_soc": "Ladezustand (SOC)",
"installation_name": "Installationsname",
"inheritedAccess": "Vererbter Zugriff von", "inheritedAccess": "Vererbter Zugriff von",
"installation": "Installation", "installation": "Installation",
"installationTabs": "Installationen", "installationTabs": "Installationen",
"installations": "Installationen", "installations": "Installationen",
"lastWeek": "Letzte Woche", "lastWeek": "Letzte Woche",
"location": "Standort", "location": "Standort",
"log": "Logbuch", "log": "Log daten",
"logout": "Abmelden", "logout": "Abmelden",
"makeASelection": "Bitte wählen Sie links eine Auswahl", "makeASelection": "Bitte wählen Sie links eine Auswahl",
"manageAccess": "Zugriff verwalten", "manageAccess": "Zugriffsverwaltung",
"move": "Verschieben", "move": "Verschieben",
"moveTo": "Verschieben zu", "moveTo": "Verschieben nach",
"moveTree": "Baum verschieben", "moveTree": "Baum verschieben",
"name": "Name", "name": "Name",
"navigationTabs": "Navigation", "navigationTabs": "Navigation",
"orderNumbers": "Bestellnummer", "orderNumbers": "Bestellnummern",
"region": "Region", "region": "Region",
"requiredLocation": "Standort ist erforderlich", "requiredLocation": "Standort ist erforderlich",
"requiredName": "Name ist erforderlich", "requiredName": "Name ist erforderlich",
"requiredRegion": "Region ist erforderlich", "requiredRegion": "Region ist erforderlich",
"search": "Suche", "search": "Suchen",
"submit": "Senden", "submit": "Senden",
"updateFolderErrorMessage": "Fehler, Ordner kann nicht aktualisiert werden", "updateFolderErrorMessage": "Fehler, Ordner kann nicht aktualisiert werden",
"updatedSuccessfully": "Erfolgreich aktualisiert", "updatedSuccessfully": "Erfolgreich aktualisiert",
@ -44,27 +67,27 @@
"userTabs": "Nutzer", "userTabs": "Nutzer",
"users": "Nutzer", "users": "Nutzer",
"status": "Status", "status": "Status",
"live": "Live Übertragung", "live": "Live Daten",
"deleteInstallation": "Installation löschen", "deleteInstallation": "Installation löschen",
"errorOccured": "Ein Fehler ist aufgetreten", "errorOccured": "Ein Fehler ist aufgetreten",
"successfullyUpdated": "Erfolgreich aktualisiert", "successfullyUpdated": "Erfolgreich aktualisiert",
"grantAccess": "Zugriff gewähren", "grantAccess": "Zugriff gewähren",
"UserswithDirectAccess": "Benutzer mit direktem Zugriff", "UserswithDirectAccess": "Benutzer mit direktem Zugriff",
"UserswithInheritedAccess": "Benutzer mit geerbtem Zugriff", "UserswithInheritedAccess": "Benutzer mit vererbtem Zugriff",
"noerrors": "Es liegen keine Fehler vor", "noerrors": "Keine Fehler",
"nowarnings": "Es gibt keine Warnungen", "nowarnings": "Keine Warnungen",
"noUsersWithDirectAccessToThis": "Keine Benutzer mit direktem Zugriff darauf ", "noUsersWithDirectAccessToThis": "Keine Benutzer mit direktem Zugriff",
"selectUsers": "Wählen Sie Benutzer aus", "selectUsers": "Benutzer auswählen",
"cancel": "Stornieren", "cancel": "Abbrechen",
"addNewFolder": "Neuen Ordner hinzufügen", "addNewFolder": "Neuen Ordner hinzufügen",
"addNewInstallation": "Neue Installation hinzufügen", "addNewInstallation": "Neue Installation hinzufügen",
"deleteFolder": "Lösche Ordner", "deleteFolder": "Ordner löschen",
"grantAccessToFolders": "Gewähren Sie Zugriff auf Ordner", "grantAccessToFolders": "Zugriff auf Ordner gewähren",
"grantAccessToInstallations": "Gewähren Sie Zugriff auf Installationen", "grantAccessToInstallations": "Zugriff auf Installationen gewähren",
"cannotloadloggingdata": "Protokollierungsdaten können nicht geladen werden", "cannotloadloggingdata": "Log Daten können nicht geladen werden",
"grantedAccessToUsers": "Benutzern Zugriff gewährt: ", "grantedAccessToUsers": "Den Benutzern wurde den Zugriff gewährt",
"unableToGrantAccess": "Der Zugriff auf kann nicht gewährt werden: ", "unableToGrantAccess": "Der Zugriff kann nicht gewährt werden",
"unableToLoadData": "Daten können nicht geladen werden", "unableToLoadData": "Daten können nicht geladen werden",
"unableToRevokeAccess": "Der Zugriff kann nicht widerrufen werden", "unableToRevokeAccess": "Der Zugriff konnte nicht widerrufen werden",
"revokedAccessFromUser": "Zugriff vom Benutzer widerrufen: " "revokedAccessFromUser": "Zugriff vom Benutzer widerrufen"
} }

View File

@ -1,42 +1,59 @@
{ {
"information": "Informations", "information": "Information",
"addNewChild": "Ajouter un nouvel enfant",
"addNewDialogButton": "Ajouter un nouveau bouton de dialogue",
"addUser": "Créer un utilisateur", "addUser": "Créer un utilisateur",
"alarms": "Alarmes", "alarms": "Alarmes",
"applyChanges": "Appliquer les modifications", "applyChanges": "Appliquer",
"country": "Pays", "country": "Pays",
"createNewFolder": "Créer un nouveau dossier", "createNewFolder": "Nouveau dossier",
"createNewUser": "Créer un nouvel utilisateur", "createNewUser": "Nouvel utilisateur",
"customerName": "Nom du client", "customerName": "Nom du client",
"email": "E-mail", "email": "Courriel",
"english": "Anglais", "english": "Anglais",
"error": "Erreur", "error": "Erreur",
"folder": "Dossier", "folder": "Dossier",
"german": "Allemand", "german": "Allemand",
"groupTabs": "Onglets de groupe", "overview": "Aperçu",
"groupTree": "Arbre de groupe", "manage": "Gestion des accès",
"configuration": "Configuration",
"installation_name": "Nom de l'installation",
"apply_changes": "Appliquer",
"delete_user": "Supprimer l'utilisateur",
"installation_name_simple": "Nom de l'installation: ",
"language": "Langue",
"minimum_soc": "Soc minimum",
"calibration_charge_forced": "Charge d'étalonnage forcée",
"grid_set_point": "Point de consigne de grid",
"Installed_Power_DC1010": "Alimentation branchée",
"Maximum_Discharge_Power": "Puissance de décharge maximale",
"Number_of_Batteries": "Nombre de batteries",
"24_hours": "24-heures",
"lastweek": "Semaine dernière",
"lastmonth": "Mois dernier",
"battery_temperature": "Température de la batterie",
"pv_production": "Production photovoltaïque",
"grid_power": "Alimentation du réseau",
"battery_power": "Puissance de la batterie",
"dc_voltage": "Tension du bus CC",
"battery_soc": "État de charge de la batterie",
"inheritedAccess": "Accès hérité de", "inheritedAccess": "Accès hérité de",
"installation": "Installation", "installation": "Installation",
"installationTabs": "Onglets d'installation",
"installations": "Installations", "installations": "Installations",
"lastWeek": "La semaine dernière", "lastWeek": "La semaine dernière",
"location": "Localisation", "location": "Locali",
"log": "Journal", "log": "Journal",
"logout": "Déconnexion", "logout": "Fermer las session",
"makeASelection": "Veuillez faire une sélection à gauche", "makeASelection": "Veuillez faire une sélection à gauche",
"manageAccess": "Gérer l'accès", "manageAccess": "Gérer l'accès",
"move": "Déplacer", "move": "Déplacer",
"moveTo": "Déplacer à", "moveTo": "Déplacer à",
"moveTree": "Déplacer l'arbre", "moveTree": "Déplacer l'arbre",
"name": "Nom", "name": "Nom",
"navigationTabs": "Onglets de navigation", "orderNumbers": "Numéros de commande",
"orderNumbers": "Numéro de commande",
"region": "Région", "region": "Région",
"requiredLocation": "L'emplacement est requis", "requiredLocation": "La localité est requis",
"requiredName": "Le nom est obligatoire", "requiredName": "Le nom est obligatoire",
"requiredRegion": "La région est obligatoire", "requiredRegion": "La région est obligatoire",
"search": "Recherche", "search": "Rechercher",
"submit": "Soumettre", "submit": "Soumettre",
"updateFolderErrorMessage": "Une erreur s'est produite, impossible de mettre à jour le dossier.", "updateFolderErrorMessage": "Une erreur s'est produite, impossible de mettre à jour le dossier.",
"updatedSuccessfully": "Mise à jour réussie", "updatedSuccessfully": "Mise à jour réussie",
@ -46,14 +63,14 @@
"status": "Statut", "status": "Statut",
"live": "Diffusion en direct", "live": "Diffusion en direct",
"deleteInstallation": "Supprimer l'installation", "deleteInstallation": "Supprimer l'installation",
"errorOccured": "Une erreur est survenue", "errorOccured": "Une erreur sest produite",
"successfullyUpdated": "Mise à jour réussie", "successfullyUpdated": "Mise à jour réussie",
"grantAccess": "Accorder l'accès", "grantAccess": "Accorder l'accès",
"UserswithDirectAccess": "Utilisateurs avec accès direct", "UserswithDirectAccess": "Utilisateurs avec accès direct",
"UserswithInheritedAccess": "Utilisateurs avec accès hérité", "UserswithInheritedAccess": "Utilisateurs avec accès hérité",
"noerrors": "Il n'y a pas d'erreurs", "noerrors": "Il n'y a pas d'erreurs",
"nowarnings": "Il n'y a aucun avertissement", "nowarnings": "Il n'y a aucun avertissement",
"noUsersWithDirectAccessToThis": "Aucun utilisateur ayant un accès direct à ceci ", "noUsersWithDirectAccessToThis": "Aucun utilisateur ayant un accès direct",
"selectUsers": "Sélectionnez les utilisateurs", "selectUsers": "Sélectionnez les utilisateurs",
"cancel": "Annuler", "cancel": "Annuler",
"addNewFolder": "Ajouter un nouveau dossier", "addNewFolder": "Ajouter un nouveau dossier",
@ -62,9 +79,9 @@
"grantAccessToFolders": "Accorder l'accès aux dossiers", "grantAccessToFolders": "Accorder l'accès aux dossiers",
"grantAccessToInstallations": "Accorder l'accès aux installations", "grantAccessToInstallations": "Accorder l'accès aux installations",
"cannotloadloggingdata": "Impossible de charger les données de journalisation", "cannotloadloggingdata": "Impossible de charger les données de journalisation",
"grantedAccessToUsers": "Accès accordé aux utilisateurs: ", "grantedAccessToUsers": "Accès accordé aux utilisateurs",
"unableToGrantAccess": "Impossible d'accorder l'accès à: ", "unableToGrantAccess": "Impossible d'accorder l'accès à",
"unableToLoadData": "Impossible de charger les données", "unableToLoadData": "Impossible de charger les données",
"unableToRevokeAccess": "Impossible de révoquer l'accès", "unableToRevokeAccess": "Impossible de révoquer l'accès",
"revokedAccessFromUser": "Accès révoqué de l'utilisateur: " "revokedAccessFromUser": "Accès révoqué de l'utilisateur"
} }

View File

@ -6,10 +6,11 @@ import {
Menu, Menu,
MenuItem MenuItem
} from '@mui/material'; } from '@mui/material';
import { useContext, useRef, useState } from 'react'; import React, { useContext, useRef, useState } from 'react';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import ExpandMoreTwoToneIcon from '@mui/icons-material/ExpandMoreTwoTone'; import ExpandMoreTwoToneIcon from '@mui/icons-material/ExpandMoreTwoTone';
import { ThemeContext } from '../../../../theme/ThemeProvider'; import { ThemeContext } from '../../../../theme/ThemeProvider';
import { FormattedMessage } from 'react-intl';
interface HeaderButtonsProps { interface HeaderButtonsProps {
language: string; language: string;
@ -102,20 +103,18 @@ function HeaderMenu(props: HeaderButtonsProps) {
handleClose(); handleClose();
}; };
const isMobile = window.innerWidth <= 1280;
return ( return (
<> <>
<ListWrapper <ListWrapper
sx={{ sx={{
display: { color: isMobile ? 'white' : ''
xs: 'none',
md: 'block'
}
}} }}
> >
<List disablePadding component={Box} display="flex"> <List disablePadding component={Box} display="flex">
<ListItem <ListItem
classes={{ root: 'MuiListItem-indicators' }} classes={{ root: 'MuiListItem-indicators' }}
button
ref={ref} ref={ref}
onClick={handleOpen} onClick={handleOpen}
> >
@ -123,7 +122,7 @@ function HeaderMenu(props: HeaderButtonsProps) {
primaryTypographyProps={{ noWrap: true }} primaryTypographyProps={{ noWrap: true }}
primary={ primary={
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
Language <FormattedMessage id="language" defaultMessage="Language" />
<Box display="flex" alignItems="center" pl={0.3}> <Box display="flex" alignItems="center" pl={0.3}>
<ExpandMoreTwoToneIcon fontSize="small" /> <ExpandMoreTwoToneIcon fontSize="small" />
</Box> </Box>

View File

@ -17,6 +17,7 @@ import axiosConfig from 'src/Resources/axiosConfig';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { TokenContext } from 'src/contexts/tokenContext'; import { TokenContext } from 'src/contexts/tokenContext';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import routes from 'src/Resources/routes.json';
const UserBoxButton = styled(Button)( const UserBoxButton = styled(Button)(
({ theme }) => ` ({ theme }) => `
@ -68,7 +69,7 @@ function HeaderUserbox() {
removeToken(); removeToken();
removeUser(); removeUser();
localStorage.removeItem('theme'); localStorage.removeItem('theme');
navigate('/'); navigate(routes.login);
}); });
}; };

View File

@ -3,6 +3,7 @@ import { useContext } from 'react';
import { import {
alpha, alpha,
Box, Box,
darken,
Divider, Divider,
IconButton, IconButton,
lighten, lighten,
@ -44,12 +45,17 @@ interface HeaderProps {
function Header(props: HeaderProps) { function Header(props: HeaderProps) {
const { sidebarToggle, toggleSidebar } = useContext(SidebarContext); const { sidebarToggle, toggleSidebar } = useContext(SidebarContext);
const theme = useTheme(); const theme = useTheme();
const isMobile = window.innerWidth <= 1280;
return ( return (
<HeaderWrapper <HeaderWrapper
display="flex" display="flex"
alignItems="center" alignItems="center"
sx={{ sx={{
backgroundColor: isMobile
? darken(theme.colors.alpha.black[100], 0.5)
: '',
boxShadow: boxShadow:
theme.palette.mode === 'dark' theme.palette.mode === 'dark'
? `0 1px 0 ${alpha( ? `0 1px 0 ${alpha(
@ -65,6 +71,23 @@ function Header(props: HeaderProps) {
)}` )}`
}} }}
> >
<Box
component="span"
sx={{
ml: 2,
display: { lg: 'none', xs: 'inline-block' }
}}
>
<Tooltip arrow title="Toggle Menu">
<IconButton sx={{ color: 'white' }} onClick={toggleSidebar}>
{!sidebarToggle ? (
<MenuTwoToneIcon fontSize="small" />
) : (
<CloseTwoToneIcon fontSize="small" />
)}
</IconButton>
</Tooltip>
</Box>
<Stack <Stack
direction="row" direction="row"
divider={<Divider orientation="vertical" flexItem />} divider={<Divider orientation="vertical" flexItem />}
@ -79,23 +102,6 @@ function Header(props: HeaderProps) {
/> />
<HeaderUserbox /> <HeaderUserbox />
<Box
component="span"
sx={{
ml: 2,
display: { lg: 'none', xs: 'inline-block' }
}}
>
<Tooltip arrow title="Toggle Menu">
<IconButton color="primary" onClick={toggleSidebar}>
{!sidebarToggle ? (
<MenuTwoToneIcon fontSize="small" />
) : (
<CloseTwoToneIcon fontSize="small" />
)}
</IconButton>
</Tooltip>
</Box>
</Box> </Box>
</HeaderWrapper> </HeaderWrapper>
); );

View File

@ -14,6 +14,7 @@ import { SidebarContext } from 'src/contexts/SidebarContext';
import BrightnessLowTwoToneIcon from '@mui/icons-material/BrightnessLowTwoTone'; import BrightnessLowTwoToneIcon from '@mui/icons-material/BrightnessLowTwoTone';
import TableChartTwoToneIcon from '@mui/icons-material/TableChartTwoTone'; import TableChartTwoToneIcon from '@mui/icons-material/TableChartTwoTone';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { UserContext } from '../../../../contexts/userContext';
const MenuWrapper = styled(Box)( const MenuWrapper = styled(Box)(
({ theme }) => ` ({ theme }) => `
@ -159,6 +160,8 @@ const SubMenuWrapper = styled(Box)(
function SidebarMenu() { function SidebarMenu() {
const { closeSidebar } = useContext(SidebarContext); const { closeSidebar } = useContext(SidebarContext);
const context = useContext(UserContext);
const { currentUser, setUser } = context;
return ( return (
<> <>
@ -188,30 +191,33 @@ function SidebarMenu() {
</List> </List>
</SubMenuWrapper> </SubMenuWrapper>
</List> </List>
<List
component="div" {currentUser.hasWriteAccess && (
subheader={ <List
<ListSubheader component="div" disableSticky> component="div"
Management subheader={
</ListSubheader> <ListSubheader component="div" disableSticky>
} Management
> </ListSubheader>
<SubMenuWrapper> }
<List component="div"> >
<ListItem component="div"> <SubMenuWrapper>
<Button <List component="div">
disableRipple <ListItem component="div">
component={RouterLink} <Button
onClick={closeSidebar} disableRipple
to="/users" component={RouterLink}
startIcon={<TableChartTwoToneIcon />} onClick={closeSidebar}
> to="/users"
<FormattedMessage id="users" defaultMessage="Users" /> startIcon={<TableChartTwoToneIcon />}
</Button> >
</ListItem> <FormattedMessage id="users" defaultMessage="Users" />
</List> </Button>
</SubMenuWrapper> </ListItem>
</List> </List>
</SubMenuWrapper>
</List>
)}
</MenuWrapper> </MenuWrapper>
</> </>
); );

View File

@ -1,13 +1,13 @@
import { useContext } from 'react'; import { useContext } from 'react';
import Scrollbar from 'src/components/Scrollbar'; import Scrollbar from 'src/components/Scrollbar';
import { SidebarContext } from 'src/contexts/SidebarContext'; import { SidebarContext } from 'src/contexts/SidebarContext';
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png'; import innovenergyLogo from 'src/Resources/images/innovenergy-Logo_Speichern-mit-Salz_R_color.svg';
import { import {
alpha, alpha,
Box, Box,
darken, darken,
Divider, Divider,
Drawer,
lighten, lighten,
styled, styled,
useTheme useTheme
@ -18,7 +18,7 @@ import SidebarMenu from './SidebarMenu';
const SidebarWrapper = styled(Box)( const SidebarWrapper = styled(Box)(
({ theme }) => ` ({ theme }) => `
width: ${theme.sidebar.width}; width: ${theme.sidebar.width};
min-width: "${theme.sidebar.width}"; min-width: ${theme.sidebar.width};
color: ${theme.colors.alpha.trueWhite[70]}; color: ${theme.colors.alpha.trueWhite[70]};
position: relative; position: relative;
z-index: 7; z-index: 7;
@ -63,8 +63,7 @@ function Sidebar() {
src={innovenergyLogo} src={innovenergyLogo}
alt="innovenergy logo" alt="innovenergy logo"
style={{ style={{
maxWidth: '150px', // Maximum width for the image width: '150px' // Width of the image
maxHeight: '150px' // Maximum height for the image
}} }}
/> />
</Box> </Box>
@ -78,12 +77,53 @@ function Sidebar() {
/> />
<SidebarMenu /> <SidebarMenu />
</Scrollbar> </Scrollbar>
<Divider
sx={{
background: theme.colors.alpha.trueWhite[10]
}}
/>
</SidebarWrapper> </SidebarWrapper>
<Drawer
sx={{
boxShadow: `${theme.sidebar.boxShadow}`
}}
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
open={sidebarToggle}
onClose={closeSidebar}
variant="temporary"
elevation={9}
>
<SidebarWrapper
sx={{
background:
theme.palette.mode === 'dark'
? theme.colors.alpha.white[100]
: darken(theme.colors.alpha.black[100], 0.5)
}}
>
<Scrollbar>
<Box mt={3}>
<Box
mx={2}
sx={{
width: 52
}}
>
<img
src={innovenergyLogo}
alt="innovenergy logo"
style={{
width: '150px' // Width of the image
}}
/>
</Box>
</Box>
<Divider
sx={{
mt: theme.spacing(3),
mx: theme.spacing(2),
background: theme.colors.alpha.trueWhite[10]
}}
/>
<SidebarMenu />
</Scrollbar>
</SidebarWrapper>
</Drawer>
</> </>
); );
} }

View File

@ -1,14 +1,14 @@
{ {
"installation": "installation/", "installation": "installation/",
"liveView": "liveView/", "liveView": "liveView/",
"users": "/users/", "users": "/users/",
"log": "log/", "log": "log/",
"installations": "/installations/", "installations": "/installations/",
"groups": "/groups/", "groups": "/groups/",
"group": "group/", "group": "group/",
"folder": "folder/", "folder": "folder/",
"manageAccess": "manageAccess/", "manageAccess": "manageAccess/",
"user": "user/", "user": "user/",
"tree": "tree/", "tree": "tree/",
"list": "list/" "list": "list/"
} }