Inserted folder view in backend-frontend

Clients can have access to both products
Apply 20min chunk logic for Salidomo installations
This commit is contained in:
Noe 2024-08-12 09:48:16 +02:00
parent 2d3af976c1
commit 7db44785be
36 changed files with 894 additions and 359 deletions

View File

@ -41,10 +41,14 @@ public class Controller : ControllerBase
throw new Exceptions(401, "Wrong Password Exception", "Please try again.", Request.Path.Value!);
}
var session = new Session(user.HidePassword().HideParentIfUserHasNoAccessToParent(user));
//TODO The Frontend should check for the MustResetPassword Flag
return Db.Create(session)
? session
: throw new Exceptions(401,"Session Creation Exception", "Not allowed to log in.", Request.Path.Value!);
@ -472,15 +476,16 @@ public class Controller : ControllerBase
[HttpGet(nameof(GetAllFoldersAndInstallations))]
public ActionResult<IEnumerable<Object>> GetAllFoldersAndInstallations(Token authToken)
public ActionResult<IEnumerable<Object>> GetAllFoldersAndInstallations(int productId, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null)
return Unauthorized();
var foldersAndInstallations = user
.AccessibleFoldersAndInstallations(product:0)
.AccessibleFoldersAndInstallations(product:productId)
.Do(o => o.FillOrderNumbers())
.Select(o => o.HideParentIfUserHasNoAccessToParent(user))
.OfType<Object>(); // Important! JSON serializer must see Objects otherwise

View File

@ -1,3 +1,3 @@
namespace InnovEnergy.App.Backend.DataTypes;
public class Folder : TreeNode {}
public class Folder : TreeNode { }

View File

@ -143,7 +143,7 @@ public static class ExoCmd
{
const String url = "https://api-ch-dk-2.exoscale.com/v2/iam-role";
const String method = "iam-role";
String rolename = installation.Product==0?Db.Installations.Count(f => f.Product == 0) + installation.InstallationName:Db.Installations.Count(f => f.Product == 1) + installation.InstallationName;
String rolename = installation.Product==0?Db.Installations.Count(f => f.Product == 0) + installation.Name:Db.Installations.Count(f => f.Product == 1) + installation.Name;
var contentString = $$"""
@ -245,7 +245,7 @@ public static class ExoCmd
{
const String url = "https://api-ch-dk-2.exoscale.com/v2/iam-role";
const String method = "iam-role";
String rolename = installation.Product==0?Db.Installations.Count(f => f.Product == 0) + installation.InstallationName:Db.Installations.Count(f => f.Product == 1) + installation.InstallationName;
String rolename = installation.Product==0?Db.Installations.Count(f => f.Product == 0) + installation.Name:Db.Installations.Count(f => f.Product == 1) + installation.Name;
var contentString = $$"""
{

View File

@ -41,9 +41,9 @@ public static class FolderMethods
public static IEnumerable<Folder> UniqueChildFolders(this Folder parent)
{
var set = new HashSet<Folder>(Db.Folders, EqualityComparer<Folder>.Default);
//var set = new HashSet<Folder>(Db.Folders, EqualityComparer<Folder>.Default);
return ChildFolders(parent).Where(set.Add);
return ChildFolders(parent);
}
public static IEnumerable<Installation> ChildInstallations(this Folder parent)
@ -56,6 +56,8 @@ public static class FolderMethods
public static IEnumerable<Folder> DescendantFolders(this Folder parent)
{
Console.WriteLine("Parent is "+parent.Id+" looking for descendant folders");
return parent
.TraverseDepthFirstPreOrder(UniqueChildFolders)
.Skip(1); // skip self

View File

@ -56,6 +56,7 @@ public static class UserMethods
public static IEnumerable<Folder> DirectlyAccessibleFolders(this User user)
{
return Db
.FolderAccess
.Where(r => r.UserId == user.Id)

View File

@ -6,11 +6,11 @@ public class User : TreeNode
{
[Unique]
public String Email { get; set; } = null!;
public int UserType { get; set; } = 0;
public Boolean MustResetPassword { get; set; } = false;
public String Language { get; set; } = null!;
public String? Password { get; set; } = null!;
public String Email { get; set; } = null!;
public int UserType { get; set; } = 0;
public Boolean MustResetPassword { get; set; } = false;
public String Language { get; set; } = null!;
public String? Password { get; set; } = null!;
[Unique]
public override String Name { get; set; } = null!;

View File

@ -0,0 +1,10 @@
namespace InnovEnergy.App.Backend.DataTypes;
public class WebsocketMessage
{
public int id { get; set; }
public int status { get; set; }
public Boolean testingMode { get; set; }
}

View File

@ -21,7 +21,8 @@ public static class Program
RabbitMqManager.InitializeEnvironment();
RabbitMqManager.StartRabbitMqConsumer();
WebsocketManager.MonitorInstallationTable();
WebsocketManager.MonitorSalimaxInstallationTable();
WebsocketManager.MonitorSalidomoInstallationTable();
builder.Services.AddControllers();

View File

@ -1,5 +1,6 @@
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.DataTypes.Methods;
using SQLite;
namespace InnovEnergy.App.Backend.Relations;
@ -11,7 +12,8 @@ public class Session : Relation<String, Int64>
[Unique ] public String Token { get => Left ; init => Left = value;}
[Indexed] public Int64 UserId { get => Right; init => Right = value;}
[Indexed] public DateTime LastSeen { get; set; }
public Boolean AccessToSalimax { get; set; } = false;
public Boolean AccessToSalidomo { get; set; } = false;
[Ignore] public Boolean Valid => DateTime.Now - LastSeen < MaxAge
&& (User) is not null;
@ -30,6 +32,9 @@ public class Session : Relation<String, Int64>
Token = CreateToken();
UserId = user.Id;
LastSeen = DateTime.Now;
AccessToSalimax = user.AccessibleInstallations(product: 0).ToList().Count > 0;
AccessToSalidomo = user.AccessibleInstallations(product: 1).ToList().Count > 0;
}
private static String CreateToken()

View File

@ -3,7 +3,8 @@ namespace InnovEnergy.App.Backend.Websockets;
public class InstallationInfo
{
public int Status { get; set; }
public DateTime Timestamp { get; set; }
public int Status { get; set; }
public DateTime Timestamp { get; set; }
public int Product { get; set; }
public List<WebSocket> Connections { get; } = new List<WebSocket>();
}

View File

@ -158,7 +158,8 @@ public static class RabbitMqManager
WebsocketManager.InstallationConnections[installationId] = new InstallationInfo
{
Status = receivedStatusMessage.Status,
Timestamp = DateTime.Now
Timestamp = DateTime.Now,
Product = installation.Product
};
}
else

View File

@ -4,6 +4,7 @@ using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.DataTypes;
namespace InnovEnergy.App.Backend.Websockets;
@ -13,12 +14,12 @@ public static class WebsocketManager
//Every 2 minutes, check the timestamp of the latest received message for every installation.
//If the difference between the two timestamps is more than two minutes, we consider this installation unavailable.
public static async Task MonitorInstallationTable()
public static async Task MonitorSalimaxInstallationTable()
{
while (true){
lock (InstallationConnections){
foreach (var installationConnection in InstallationConnections){
if ((DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(1)){
if (installationConnection.Value.Product==0 && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(1)){
installationConnection.Value.Status = -1;
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);}
}
@ -28,6 +29,21 @@ public static class WebsocketManager
}
}
public static async Task MonitorSalidomoInstallationTable()
{
while (true){
lock (InstallationConnections){
foreach (var installationConnection in InstallationConnections){
if (installationConnection.Value.Product==1 && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(20)){
installationConnection.Value.Status = -1;
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);}
}
}
}
await Task.Delay(TimeSpan.FromMinutes(30));
}
}
//Inform all the connected websockets regarding installation "installationId"
public static void InformWebsocketsForInstallation(Int64 installationId)
{
@ -35,7 +51,6 @@ public static class WebsocketManager
var installationConnection = InstallationConnections[installationId];
Console.WriteLine("Update all the connected websockets for installation " + installationId);
var jsonObject = new
{
id = installationId,
@ -96,6 +111,9 @@ public static class WebsocketManager
//Console.WriteLine("Received a new message from websocket");
lock (InstallationConnections)
{
List<WebsocketMessage> dataToSend = new List<WebsocketMessage>();
//Each front-end will send the list of the installations it wants to access
//If this is a new key (installation id), initialize the list for this key and then add the websocket object for this client
//Then, report the status of each requested installation to the front-end that created the websocket connection
@ -108,29 +126,42 @@ public static class WebsocketManager
//Console.WriteLine("Create new empty list for installation id " + installationId);
InstallationConnections[installationId] = new InstallationInfo
{
Status = -1
Status = -1,
Product = installation.Product
};
}
InstallationConnections[installationId].Connections.Add(currentWebSocket);
var jsonObject = new
var jsonObject = new WebsocketMessage
{
id = installationId,
status = InstallationConnections[installationId].Status,
testingMode = installation.TestingMode
};
var jsonString = JsonSerializer.Serialize(jsonObject);
var dataToSend = Encoding.UTF8.GetBytes(jsonString);
dataToSend.Add(jsonObject);
//var jsonString = JsonSerializer.Serialize(jsonObject);
//var dataToSend = Encoding.UTF8.GetBytes(jsonString);
currentWebSocket.SendAsync(dataToSend,
WebSocketMessageType.Text,
true, // Indicates that this is the end of the message
CancellationToken.None
);
// currentWebSocket.SendAsync(dataToSend,
// WebSocketMessageType.Text,
// true, // Indicates that this is the end of the message
// CancellationToken.None
// );
}
var jsonString = JsonSerializer.Serialize(dataToSend);
var encodedDataToSend = Encoding.UTF8.GetBytes(jsonString);
currentWebSocket.SendAsync(encodedDataToSend,
WebSocketMessageType.Text,
true, // Indicates that this is the end of the message
CancellationToken.None
);
Console.WriteLine("Printing installation connection list");
Console.WriteLine("----------------------------------------------");

View File

@ -10,4 +10,5 @@ Salimax0006 ie-entwicklung@10.2.4.35 Steakhouse Mettmenstetten
Salimax0007 ie-entwicklung@10.2.4.154 LerchenhofHerr Twannberg
Salimax0008 ie-entwicklung@10.2.4.113 Wittmann Kottingbrunn
Salimax0010 ie-entwicklung@10.2.4.211 Mohatech 1 (Beat Moser)
Salimax0011 ie-entwicklung@10.2.4.239 Thomas Tschirren (Enggistein)
SalidomoServer ig@134.209.238.170

View File

@ -54,6 +54,6 @@ INNOVENERGY_PROTOCOL_VERSION = '48TL200V3'
# S3 Credentials
S3BUCKET = "114-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
S3KEY = "EXO5b9198dc11544f42b44e1180"
S3SECRET = "ga-mD3SYZMJUfjksmXPKAHQhVkxPZYv57jC1oD_mkC0"
S3BUCKET = "91-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
S3KEY = "EXOe6dce12288f11a676c2025a1"
S3SECRET = "xpqM4Eh0Gg1HaYVkzlR9X6PwYa-QNb-mVk0XUkwW3cc"

View File

@ -24,6 +24,7 @@ import { useNavigate } from 'react-router-dom';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
import routes from 'src/Resources/routes.json';
import { ProductIdContext } from '../contexts/ProductIdContextProvider';
function Login() {
const [username, setUsername] = useState('');
@ -35,6 +36,8 @@ function Login() {
const theme = useTheme();
const context = useContext(UserContext);
const { setAccessToSalimax, setAccessToSalidomo } =
useContext(ProductIdContext);
const navigate = useNavigate();
if (!context) {
@ -73,11 +76,18 @@ function Login() {
setNewToken(response.data.token);
setUser(response.data.user);
setAccessToSalimax(response.data.accessToSalimax);
setAccessToSalidomo(response.data.accessToSalidomo);
if (rememberMe) {
cookies.set('rememberedUsername', username, { path: '/' });
cookies.set('rememberedPassword', password, { path: '/' });
}
navigate(routes.installations);
if (response.data.accessToSalimax) {
navigate(routes.installations);
} else {
navigate(routes.salidomo_installations);
}
}
})
.catch((error) => {

View File

@ -582,6 +582,7 @@ function DetailedBatteryView(props: DetailedBatteryViewProps) {
<MenuItem disabled value="">
Select Firmware Version
</MenuItem>
<MenuItem value="AF09">AF09</MenuItem>
<MenuItem value="AF11">AF11</MenuItem>
<MenuItem value="AF0A">AF0A</MenuItem>
</Select>

View File

@ -200,7 +200,7 @@ function InformationSalidomo(props: InformationSalidomoProps) {
/>
}
name="installationName"
value={formValues.installationName}
value={formValues.name}
onChange={handleChange}
variant="outlined"
fullWidth

View File

@ -379,6 +379,7 @@ function Installation(props: singleInstallationProps) {
{loading &&
currentTab != 'information' &&
currentTab != 'history' &&
currentTab != 'manage' &&
currentTab != 'overview' &&
currentTab != 'log' && (
<Container

View File

@ -53,9 +53,12 @@ export const fetchAggregatedData = (
export const fetchData = (
timestamp: UnixTime,
s3Credentials?: I_S3Credentials
s3Credentials?: I_S3Credentials,
cutdigits?: boolean
): Promise<FetchResult<Record<string, DataRecord>>> => {
const s3Path = `${timestamp.ticks}.csv`;
const s3Path = cutdigits
? `${timestamp.ticks.toString().slice(0, -2)}.csv`
: `${timestamp.ticks}.csv`;
if (s3Credentials && s3Credentials.s3Bucket) {
const s3Access = new S3Access(
s3Credentials.s3Bucket,
@ -71,10 +74,14 @@ export const fetchData = (
if (r.status === 404) {
return Promise.resolve(FetchResult.notAvailable);
} else if (r.status === 200) {
console.log('FOUND ITTTTTTTTTTTT');
const csvtext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text
const contentEncoding = r.headers.get('content-type');
console.log(contentEncoding);
if (contentEncoding != 'application/base64; charset=utf-8') {
console.log('uncompressed');
return parseChunk(csvtext);
}
@ -87,6 +94,8 @@ export const fetchData = (
// Assuming the CSV file is named "data.csv" inside the ZIP archive
const csvContent = await zip.file('data.csv').async('text');
console.log(csvContent);
return parseChunk(csvContent);
} else {
return Promise.resolve(FetchResult.notAvailable);

View File

@ -14,6 +14,7 @@ import { InstallationsContext } from '../../../contexts/InstallationsContextProv
import Installation from './Installation';
import { WebSocketContext } from '../../../contexts/WebSocketContextProvider';
import { UserType } from '../../../interfaces/UserTypes';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
function InstallationTabs() {
const location = useLocation();
@ -35,6 +36,8 @@ function InstallationTabs() {
const { salimaxInstallations, fetchAllInstallations } =
useContext(InstallationsContext);
const { product, setProduct } = useContext(ProductIdContext);
const webSocketsContext = useContext(WebSocketContext);
const { socket, openSocket, closeSocket } = webSocketsContext;
@ -67,6 +70,10 @@ function InstallationTabs() {
}
}, [salimaxInstallations]);
useEffect(() => {
setProduct(0);
}, []);
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value);
};

View File

@ -1291,6 +1291,140 @@ function Overview(props: OverviewProps) {
</Card>
</Grid>
</Grid>
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item md={6} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '30px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="ac_load"
defaultMessage="AC Load"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
{dailyData && (
<ReactApexChart
options={{
...getChartOptions(
dailyDataArray[chartState].chartOverview.ACLoad,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: (chartContext, options) => {
startZoom();
handleBeforeZoom(chartContext, options);
}
}
}
}}
series={[
{
...dailyDataArray[chartState].chartData.ACLoad,
color: '#ff9900'
}
]}
type="line"
height={400}
/>
)}
</Card>
</Grid>
<Grid item md={6} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '30px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="dc_load"
defaultMessage="DC Load"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
{dailyData && (
<ReactApexChart
options={{
...getChartOptions(
dailyDataArray[chartState].chartOverview.DCLoad,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: (chartContext, options) => {
startZoom();
handleBeforeZoom(chartContext, options);
}
}
}
}}
series={[
{
...dailyDataArray[chartState].chartData.DCLoad,
color: '#ff3333'
}
]}
type="line"
height={400}
/>
)}
</Card>
</Grid>
</Grid>
</Grid>
)}
</Grid>

View File

@ -157,7 +157,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
noWrap
sx={{ marginTop: '10px', fontSize: 'small' }}
>
{installation.installationName}
{installation.name}
</Typography>
</TableCell>

View File

@ -27,13 +27,15 @@ import SalidomoOverview from '../Overview/salidomoOverview';
import { UserType } from '../../../interfaces/UserTypes';
import HistoryOfActions from '../History/History';
import BuildIcon from '@mui/icons-material/Build';
import AccessContextProvider from '../../../contexts/AccessContextProvider';
import Access from '../ManageAccess/Access';
interface singleInstallationProps {
current_installation?: I_Installation;
type?: string;
}
function Installation(props: singleInstallationProps) {
function SalidomoInstallation(props: singleInstallationProps) {
const context = useContext(UserContext);
const { currentUser } = context;
const location = useLocation().pathname;
@ -77,18 +79,19 @@ function Installation(props: singleInstallationProps) {
const continueFetching = useRef(false);
const fetchDataPeriodically = async () => {
var timeperiodToSearch = 90;
var timeperiodToSearch = 30;
let res;
let timestampToFetch;
for (var i = 0; i < timeperiodToSearch; i += 2) {
for (var i = 0; i < timeperiodToSearch; i += 1) {
if (!continueFetching.current) {
return false;
}
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromMinutes(i));
console.log('timestamp to fetch is ' + timestampToFetch);
try {
res = await fetchData(timestampToFetch, s3Credentials);
res = await fetchData(timestampToFetch, s3Credentials, true);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
break;
}
@ -127,7 +130,7 @@ function Installation(props: singleInstallationProps) {
await timeout(2000);
}
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(60));
timestampToFetch = timestampToFetch.later(TimeSpan.fromMinutes(20));
console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch);
for (i = 0; i < 10; i++) {
@ -137,7 +140,7 @@ function Installation(props: singleInstallationProps) {
try {
console.log('Trying to fetch timestamp ' + timestampToFetch);
res = await fetchData(timestampToFetch, s3Credentials);
res = await fetchData(timestampToFetch, s3Credentials, true);
if (
res !== FetchResult.notAvailable &&
res !== FetchResult.tryLater
@ -148,7 +151,7 @@ function Installation(props: singleInstallationProps) {
console.error('Error fetching data:', err);
return false;
}
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1));
timestampToFetch = timestampToFetch.later(TimeSpan.fromMinutes(1));
}
}
};
@ -213,7 +216,7 @@ function Installation(props: singleInstallationProps) {
fontSize: '14px'
}}
>
{props.current_installation.installationName}
{props.current_installation.name}
</Typography>
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
@ -296,6 +299,7 @@ function Installation(props: singleInstallationProps) {
{loading &&
currentTab != 'information' &&
currentTab != 'overview' &&
currentTab != 'manage' &&
currentTab != 'history' &&
currentTab != 'log' && (
<Container
@ -384,9 +388,23 @@ function Installation(props: singleInstallationProps) {
/>
)}
{currentUser.userType == UserType.admin && (
<Route
path={routes.manage}
element={
<AccessContextProvider>
<Access
currentResource={props.current_installation}
resourceType={props.type}
></Access>
</AccessContextProvider>
}
/>
)}
<Route
path={'*'}
element={<Navigate to={routes.information}></Navigate>}
element={<Navigate to={routes.batteryview}></Navigate>}
/>
</Routes>
</Grid>
@ -396,4 +414,4 @@ function Installation(props: singleInstallationProps) {
);
}
export default Installation;
export default SalidomoInstallation;

View File

@ -5,10 +5,7 @@ import FlatInstallationView from './FlatInstallationView';
import { I_Installation } from '../../../interfaces/InstallationTypes';
import { Route, Routes, useLocation } from 'react-router-dom';
import routes from '../../../Resources/routes.json';
import Installation from './Installation';
import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button';
import SalidomonstallationForm from './SalidomoInstallationForm';
import SalidomoInstallation from './Installation';
interface installationSearchProps {
installations: I_Installation[];
@ -18,26 +15,11 @@ function InstallationSearch(props: installationSearchProps) {
const [searchTerm, setSearchTerm] = useState('');
const currentLocation = useLocation();
const [filteredData, setFilteredData] = useState(props.installations);
const [openModalInstallation, setOpenModalInstallation] = useState(false);
const handleNewInstallationInsertion = () => {
setOpenModalInstallation(true);
};
const handleInstallationFormSubmit = () => {
setOpenModalInstallation(false);
};
const handleFormCancel = () => {
setOpenModalInstallation(false);
};
useEffect(() => {
const filtered = props.installations.filter(
(item) =>
item.installationName
.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.location.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.region.toLowerCase().includes(searchTerm.toLowerCase())
);
@ -46,12 +28,6 @@ function InstallationSearch(props: installationSearchProps) {
return (
<>
{openModalInstallation && (
<SalidomonstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}
/>
)}
<Grid container>
<Grid
item
@ -74,17 +50,6 @@ function InstallationSearch(props: installationSearchProps) {
alignItems: 'flex-start'
}}
>
<Button
variant="contained"
onClick={handleNewInstallationInsertion}
sx={{ marginBottom: '8px' }}
>
<FormattedMessage
id="addNewInstallation"
defaultMessage="Add new installation"
/>
</Button>
<FormControl variant="outlined">
<TextField
placeholder="Search"
@ -112,11 +77,11 @@ function InstallationSearch(props: installationSearchProps) {
key={installation.s3BucketId}
path={routes.installation + installation.s3BucketId + '*'}
element={
<Installation
<SalidomoInstallation
key={installation.s3BucketId}
current_installation={installation}
type="installation"
></Installation>
></SalidomoInstallation>
}
/>
);

View File

@ -21,26 +21,21 @@ import { FormattedMessage } from 'react-intl';
interface SalidomoInstallationFormProps {
cancel: () => void;
submit: () => void;
parentid: number;
}
function SalidomonstallationForm(props: SalidomoInstallationFormProps) {
const theme = useTheme();
const [open, setOpen] = useState(true);
const [formValues, setFormValues] = useState<Partial<I_Installation>>({
installationName: '',
name: '',
region: '',
location: '',
country: '',
vpnIp: '',
vrmLink: ''
});
const requiredFields = [
'installationName',
'location',
'country',
'vpnIp',
'vrmLink'
];
const requiredFields = ['name', 'location', 'country', 'vpnIp', 'vrmLink'];
const DeviceTypes = ['Cerbo', 'Venus'];
const installationContext = useContext(InstallationsContext);
@ -57,7 +52,7 @@ function SalidomonstallationForm(props: SalidomoInstallationFormProps) {
};
const handleSubmit = async (e) => {
setLoading(true);
formValues.parentId = 1;
formValues.parentId = props.parentid;
formValues.product = 1;
const responseData = await createInstallation(formValues);
props.submit();
@ -120,11 +115,11 @@ function SalidomonstallationForm(props: SalidomoInstallationFormProps) {
defaultMessage="Installation Name"
/>
}
name="installationName"
value={formValues.installationName}
name="name"
value={formValues.name}
onChange={handleChange}
required
error={formValues.installationName === ''}
error={formValues.name === ''}
/>
</div>
<div>

View File

@ -9,12 +9,24 @@ import { UserContext } from '../../../contexts/userContext';
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
import { WebSocketContext } from '../../../contexts/WebSocketContextProvider';
import ListIcon from '@mui/icons-material/List';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import TreeView from '../Tree/treeView';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
import { UserType } from '../../../interfaces/UserTypes';
import SalidomoInstallation from './Installation';
function SalidomoInstallationTabs() {
const location = useLocation();
const context = useContext(UserContext);
const { currentUser } = context;
const tabList = ['batteryview', 'information', 'overview', 'log', 'history'];
const tabList = [
'batteryview',
'information',
'manage',
'overview',
'log',
'history'
];
const [currentTab, setCurrentTab] = useState<string>(undefined);
const [fetchedInstallations, setFetchedInstallations] =
@ -22,6 +34,8 @@ function SalidomoInstallationTabs() {
const { salidomoInstallations, fetchAllSalidomoInstallations } =
useContext(InstallationsContext);
const { product, setProduct } = useContext(ProductIdContext);
const webSocketsContext = useContext(WebSocketContext);
const { socket, openSocket, closeSocket } = webSocketsContext;
@ -30,6 +44,8 @@ function SalidomoInstallationTabs() {
if (path[path.length - 2] === 'list') {
setCurrentTab('list');
} else if (path[path.length - 2] === 'tree') {
setCurrentTab('tree');
} else {
//Even if we are located at path: /batteryview/mainstats, we want the BatteryView tab to be bold
setCurrentTab(path.find((pathElement) => tabList.includes(pathElement)));
@ -52,6 +68,10 @@ function SalidomoInstallationTabs() {
}
}, [salidomoInstallations]);
useEffect(() => {
setProduct(1);
}, []);
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value);
};
@ -75,14 +95,9 @@ function SalidomoInstallationTabs() {
return ret_path;
};
const tabs =
currentTab != 'list' && !location.pathname.includes('folder')
const singleInstallationTabs =
currentUser.userType == UserType.admin
? [
{
value: 'list',
icon: <ListIcon id="mode-toggle-button-list-icon" />
},
{
value: 'batteryview',
label: (
@ -101,6 +116,16 @@ function SalidomoInstallationTabs() {
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'manage',
label: (
<FormattedMessage
id="manage"
defaultMessage="Access Management"
/>
)
},
{
value: 'information',
label: (
@ -119,71 +144,247 @@ function SalidomoInstallationTabs() {
]
: [
{
value: 'list',
icon: <ListIcon id="mode-toggle-button-list-icon" />
value: 'batteryview',
label: (
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
}
];
return (
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
<TabsContainerWrapper>
<Tabs
onChange={handleTabsChange}
value={currentTab}
variant="scrollable"
scrollButtons="auto"
textColor="primary"
indicatorColor="primary"
>
{tabs.map((tab) => (
<Tab
key={tab.value}
value={tab.value}
icon={tab.icon}
component={Link}
label={tab.label}
to={
tab.value === 'list'
? routes[tab.value]
: navigateToTabPath(location.pathname, 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}>
<InstallationSearch installations={salidomoInstallations} />
</Box>
</Grid>
}
/>
const tabs =
currentTab != 'list' &&
currentTab != 'tree' &&
!location.pathname.includes('folder') &&
currentUser.userType == UserType.admin
? [
{
value: 'list',
icon: <ListIcon id="mode-toggle-button-list-icon" />
},
{
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
},
{
value: 'batteryview',
label: (
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
<Route
path={'*'}
element={
<Navigate
to={routes.salidomo_installations + routes.list}
></Navigate>
}
></Route>
</Routes>
</Grid>
</Card>
</Container>
);
{
value: 'manage',
label: (
<FormattedMessage
id="manage"
defaultMessage="Access Management"
/>
)
},
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
},
{
value: 'history',
label: (
<FormattedMessage
id="history"
defaultMessage="History Of Actions"
/>
)
}
]
: currentTab != 'list' &&
currentTab != 'tree' &&
!location.pathname.includes('folder') &&
currentUser.userType == UserType.client
? [
{
value: 'list',
icon: <ListIcon id="mode-toggle-button-list-icon" />
},
{
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
},
{
value: 'batteryview',
label: (
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
{
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 salidomoInstallations.length > 1 ? (
<>
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
<TabsContainerWrapper>
<Tabs
onChange={handleTabsChange}
value={currentTab}
variant="scrollable"
scrollButtons="auto"
textColor="primary"
indicatorColor="primary"
>
{tabs.map((tab) => (
<Tab
key={tab.value}
value={tab.value}
icon={tab.icon}
component={Link}
label={tab.label}
to={
tab.value === 'list' || tab.value === 'tree'
? routes[tab.value]
: navigateToTabPath(location.pathname, 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}>
<InstallationSearch
installations={salidomoInstallations}
/>
</Box>
</Grid>
}
/>
<Route path={routes.tree + '*'} element={<TreeView />} />
<Route
path={'*'}
element={
<Navigate
to={routes.salidomo_installations + routes.list}
></Navigate>
}
></Route>
</Routes>
</Grid>
</Card>
</Container>
</>
) : salidomoInstallations.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={routes[tab.value]}
/>
))}
</Tabs>
</TabsContainerWrapper>
<Card variant="outlined">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={0}
>
<Routes>
<Route
path={'*'}
element={
<Grid item xs={12}>
<Box p={4}>
<SalidomoInstallation
current_installation={salidomoInstallations[0]}
type="installation"
></SalidomoInstallation>
</Box>
</Grid>
}
/>
</Routes>
</Grid>
</Card>
</Container>
</>
) : null;
}
export default SalidomoInstallationTabs;

View File

@ -10,6 +10,7 @@ import CancelIcon from '@mui/icons-material/Cancel';
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
import routes from 'src/Resources/routes.json';
import { useLocation, useNavigate } from 'react-router-dom';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
interface CustomTreeItemProps {
node: I_Installation | I_Folder;
@ -44,12 +45,15 @@ function CustomTreeItem(props: CustomTreeItemProps) {
const navigate = useNavigate();
const [selected, setSelected] = useState(false);
const currentLocation = useLocation();
const { product, setProduct } = useContext(ProductIdContext);
const handleSelectOneInstallation = (): void => {
let installation = props.node;
let path =
product == 0 ? routes.installations : routes.salidomo_installations;
if (installation.type != 'Folder') {
navigate(
routes.installations +
path +
routes.tree +
routes.installation +
installation.s3BucketId +
@ -62,7 +66,7 @@ function CustomTreeItem(props: CustomTreeItemProps) {
setSelected(!selected);
} else {
navigate(
routes.installations +
path +
routes.tree +
routes.folder +
installation.id +
@ -156,7 +160,8 @@ function CustomTreeItem(props: CustomTreeItemProps) {
}
sx={{
display:
currentLocation.pathname === routes.installations + 'tree' ||
currentLocation.pathname ===
routes.salidomo_installations + routes.tree ||
currentLocation.pathname === routes.installations + routes.tree ||
currentLocation.pathname.includes('folder')
? 'block'

View File

@ -21,6 +21,8 @@ import FolderForm from './folderForm';
import InstallationForm from '../Installations/installationForm';
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
import { UserType } from '../../../interfaces/UserTypes';
import SalidomoInstallationForm from '../SalidomoInstallations/SalidomoInstallationForm';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
interface TreeInformationProps {
folder: I_Folder;
@ -50,6 +52,8 @@ function TreeInformation(props: TreeInformationProps) {
deleteFolder
} = installationContext;
const { product, setProduct } = useContext(ProductIdContext);
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
@ -61,7 +65,7 @@ function TreeInformation(props: TreeInformationProps) {
const handleFolderInformationUpdate = () => {
setLoading(true);
setError(false);
updateFolder(formValues);
updateFolder(formValues, product);
};
const handleNewInstallationInsertion = () => {
@ -80,7 +84,7 @@ function TreeInformation(props: TreeInformationProps) {
const deleteFolderModalHandle = () => {
setOpenModalDeleteFolder(false);
deleteFolder(formValues);
deleteFolder(formValues, product);
setLoading(false);
};
@ -195,13 +199,20 @@ function TreeInformation(props: TreeInformationProps) {
parentid={props.folder.id}
/>
)}
{openModalInstallation && (
{openModalInstallation && product == 0 && (
<InstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}
parentid={props.folder.id}
/>
)}
{openModalInstallation && product == 1 && (
<SalidomoInstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}
parentid={props.folder.id}
/>
)}
<Container maxWidth="xl">
<Grid
container

View File

@ -8,19 +8,27 @@ import Installation from '../Installations/Installation';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import { Route, Routes } from 'react-router-dom';
import routes from '../../../Resources/routes.json';
import Folder from './Folder';
import { WebSocketContext } from '../../../contexts/WebSocketContextProvider';
import SalidomoInstallation from '../SalidomoInstallations/Installation';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
import Folder from './Folder';
function InstallationTree() {
const { foldersAndInstallations, fetchAllFoldersAndInstallations } =
useContext(InstallationsContext);
const { product, setProduct } = useContext(ProductIdContext);
const webSocketContext = useContext(WebSocketContext);
const { getStatus } = webSocketContext;
const sortedInstallations = [...foldersAndInstallations].sort((a, b) => {
// Compare the status field of each installation and sort them based on the status.
//Installations with alarms go first
if (a.type == 'Folder') {
return -1;
}
let a_status = getStatus(a.id);
let b_status = getStatus(b.id);
@ -34,7 +42,7 @@ function InstallationTree() {
});
useEffect(() => {
fetchAllFoldersAndInstallations();
fetchAllFoldersAndInstallations(product);
}, []);
const TreeNode = ({ node, parent_id }) => {
@ -61,17 +69,59 @@ function InstallationTree() {
</CustomTreeItem>
)
);
} else {
} else if (node.type == 'Installation') {
return (
node.parentId == parent_id && (
<CustomTreeItem node={node} parent_id={parent_id} />
)
);
} else {
return null;
}
};
return (
<Grid container spacing={1} sx={{ marginTop: 0.1 }}>
<Routes>
{foldersAndInstallations.map((installation) => {
if (installation.type == 'Installation') {
return (
<Route
key={installation.s3BucketId}
path={routes.installation + installation.s3BucketId + '*'}
element={
product == 0 ? (
<Installation
key={installation.s3BucketId}
current_installation={installation}
type="installation"
></Installation>
) : (
<SalidomoInstallation
key={installation.s3BucketId}
current_installation={installation}
type="installation"
></SalidomoInstallation>
)
}
/>
);
} else {
return (
<Route
key={installation.id}
path={routes.folder + installation.id + '*'}
element={
<Folder
key={installation.id + installation.type}
current_folder={installation}
></Folder>
}
/>
);
}
})}
</Routes>
<Grid item xs={12} md={12}>
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
@ -93,39 +143,6 @@ function InstallationTree() {
})}
</TreeView>
</Grid>
<Routes>
{foldersAndInstallations.map((installation) => {
if (installation.type == 'Installation') {
return (
<Route
key={installation.s3BucketId}
path={routes.installation + installation.s3BucketId + '*'}
element={
<Installation
key={installation.s3BucketId}
current_installation={installation}
type="installation"
></Installation>
}
/>
);
} else {
return (
<Route
key={installation.id}
path={routes.folder + installation.id + '*'}
element={
<Folder
key={installation.id + installation.type}
current_folder={installation}
></Folder>
}
/>
);
}
})}
</Routes>
</Grid>
);
}

View File

@ -14,6 +14,7 @@ import { I_Folder } from 'src/interfaces/InstallationTypes';
import { TokenContext } from 'src/contexts/tokenContext';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import { FormattedMessage } from 'react-intl';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
interface folderFormProps {
cancel: () => void;
@ -29,7 +30,7 @@ function folderForm(props: folderFormProps) {
information: ''
});
const requiredFields = ['name'];
const { product, setProduct } = useContext(ProductIdContext);
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
@ -47,7 +48,7 @@ function folderForm(props: folderFormProps) {
const handleSubmit = async (e) => {
formValues.parentId = props.parentid;
setLoading(true);
const responseData = await createFolder(formValues);
const responseData = await createFolder(formValues, product);
props.submit();
};

View File

@ -18,7 +18,7 @@ interface I_InstallationContextProviderProps {
foldersAndInstallations: I_Installation[];
fetchAllInstallations: () => Promise<void>;
fetchAllSalidomoInstallations: () => Promise<void>;
fetchAllFoldersAndInstallations: () => Promise<void>;
fetchAllFoldersAndInstallations: (product: number) => Promise<void>;
createInstallation: (value: Partial<I_Installation>) => Promise<void>;
updateInstallation: (value: I_Installation, view: string) => Promise<void>;
loading: boolean;
@ -28,9 +28,9 @@ interface I_InstallationContextProviderProps {
updated: boolean;
setUpdated: (value: boolean) => void;
deleteInstallation: (value: I_Installation, view: string) => Promise<void>;
createFolder: (value: Partial<I_Folder>) => Promise<void>;
updateFolder: (value: I_Folder) => Promise<void>;
deleteFolder: (value: I_Folder) => Promise<void>;
createFolder: (value: Partial<I_Folder>, product: number) => Promise<void>;
updateFolder: (value: I_Folder, product: number) => Promise<void>;
deleteFolder: (value: I_Folder, product: number) => Promise<void>;
}
export const InstallationsContext =
@ -40,7 +40,7 @@ export const InstallationsContext =
foldersAndInstallations: [],
fetchAllInstallations: () => Promise.resolve(),
fetchAllSalidomoInstallations: () => Promise.resolve(),
fetchAllFoldersAndInstallations: () => Promise.resolve(),
fetchAllFoldersAndInstallations: (product: number) => Promise.resolve(),
createInstallation: () => Promise.resolve(),
updateInstallation: () => Promise.resolve(),
loading: false,
@ -104,19 +104,22 @@ const InstallationsContextProvider = ({
});
}, []);
const fetchAllFoldersAndInstallations = useCallback(async () => {
return axiosConfig
.get('/GetAllFoldersAndInstallations')
.then((res) => {
setFoldersAndInstallations(res.data);
})
.catch((err) => {
if (err.response && err.response.status == 401) {
removeToken();
navigate(routes.login);
}
});
}, []);
const fetchAllFoldersAndInstallations = useCallback(
async (product: number) => {
return axiosConfig
.get(`/GetAllFoldersAndInstallations?productId=${product}`)
.then((res) => {
setFoldersAndInstallations(res.data);
})
.catch((err) => {
if (err.response && err.response.status == 401) {
removeToken();
navigate(routes.login);
}
});
},
[]
);
const createInstallation = useCallback(
async (formValues: Partial<I_Installation>) => {
@ -124,11 +127,12 @@ const InstallationsContextProvider = ({
.post('/CreateInstallation', formValues)
.then((res) => {
setLoading(false);
if (formValues.product == 0) {
fetchAllFoldersAndInstallations();
} else {
fetchAllSalidomoInstallations();
}
fetchAllFoldersAndInstallations(formValues.product);
// if (formValues.product == 0) {
// fetchAllFoldersAndInstallations();
// } else {
// fetchAllSalidomoInstallations();
// }
})
.catch((error) => {
@ -151,14 +155,12 @@ const InstallationsContextProvider = ({
if (response) {
setLoading(false);
setUpdated(true);
if (formValues.product == 0) {
if (view == 'installation') {
fetchAllInstallations();
} else {
fetchAllFoldersAndInstallations();
}
} else {
if (formValues.product == 0 && view == 'installation') {
fetchAllInstallations();
} else if (formValues.product == 1 && view == 'installation') {
fetchAllSalidomoInstallations();
} else {
fetchAllFoldersAndInstallations(formValues.product);
}
setTimeout(() => {
@ -186,14 +188,12 @@ const InstallationsContextProvider = ({
if (response) {
setLoading(false);
setUpdated(true);
if (formValues.product == 0) {
if (view == 'installation') {
fetchAllInstallations();
} else {
fetchAllFoldersAndInstallations();
}
} else {
if (formValues.product == 0 && view == 'installation') {
fetchAllInstallations();
} else if (formValues.product == 1 && view == 'installation') {
fetchAllSalidomoInstallations();
} else {
fetchAllFoldersAndInstallations(formValues.product);
}
setTimeout(() => {
@ -213,71 +213,80 @@ const InstallationsContextProvider = ({
[]
);
const createFolder = useCallback(async (formValues: Partial<I_Folder>) => {
axiosConfig
.post('/CreateFolder', formValues)
.then((res) => {
setLoading(false);
fetchAllFoldersAndInstallations();
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
navigate(routes.login);
}
});
}, []);
const updateFolder = useCallback(async (formValues: I_Folder) => {
axiosConfig
.put('/UpdateFolder', formValues)
.then((response) => {
if (response) {
const createFolder = useCallback(
async (formValues: Partial<I_Folder>, product: number) => {
axiosConfig
.post('/CreateFolder', formValues)
.then((res) => {
setLoading(false);
setUpdated(true);
fetchAllFoldersAndInstallations();
fetchAllFoldersAndInstallations(product);
})
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
navigate(routes.login);
}
});
}, []);
const deleteFolder = useCallback(async (formValues: I_Folder) => {
axiosConfig
.delete(`/DeleteFolder?folderId=${formValues.id}`)
.then((response) => {
if (response) {
.catch((error) => {
setLoading(false);
setUpdated(true);
fetchAllFoldersAndInstallations();
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
navigate(routes.login);
}
});
},
[]
);
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
navigate(routes.login);
}
});
}, []);
const updateFolder = useCallback(
async (formValues: I_Folder, product: number) => {
axiosConfig
.put('/UpdateFolder', formValues)
.then((response) => {
if (response) {
setLoading(false);
setUpdated(true);
fetchAllFoldersAndInstallations(product);
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
navigate(routes.login);
}
});
},
[]
);
const deleteFolder = useCallback(
async (formValues: I_Folder, product: number) => {
axiosConfig
.delete(`/DeleteFolder?folderId=${formValues.id}`)
.then((response) => {
if (response) {
setLoading(false);
setUpdated(true);
fetchAllFoldersAndInstallations(product);
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
navigate(routes.login);
}
});
},
[]
);
return (
<InstallationsContext.Provider

View File

@ -0,0 +1,66 @@
import { createContext, ReactNode, useState } from 'react';
import { useLocation } from 'react-router-dom';
// Define the shape of the context
interface ProductIdContextType {
product?: number;
setProduct: (new_product: number) => void;
accessToSalimax: boolean;
accessToSalidomo: boolean;
setAccessToSalimax: (access: boolean) => void;
setAccessToSalidomo: (access: boolean) => void;
}
// Create the context.
export const ProductIdContext = createContext<ProductIdContextType | undefined>(
undefined
);
// Create a UserContextProvider component
export const ProductIdContextProvider = ({
children
}: {
children: ReactNode;
}) => {
const location = useLocation().pathname;
const [accessToSalimax, setAccessToSalimax] = useState(() => {
const storedValue = localStorage.getItem('accessToSalimax');
return storedValue === 'true';
});
const [accessToSalidomo, setAccessToSalidomo] = useState(() => {
const storedValue = localStorage.getItem('accessToSalidomo');
return storedValue === 'true';
});
const [product, setProduct] = useState(location.includes('salidomo') ? 1 : 0);
const changeProductId = (new_product: number) => {
setProduct(new_product);
};
const changeAccessSalimax = (access: boolean) => {
setAccessToSalimax(access);
localStorage.setItem('accessToSalimax', JSON.stringify(access));
};
const changeAccessSalidomo = (access: boolean) => {
setAccessToSalidomo(access);
localStorage.setItem('accessToSalidomo', JSON.stringify(access));
};
return (
<ProductIdContext.Provider
value={{
product,
setProduct: changeProductId,
accessToSalimax,
accessToSalidomo,
setAccessToSalimax: changeAccessSalimax,
setAccessToSalidomo: changeAccessSalidomo
}}
>
{children}
</ProductIdContext.Provider>
);
};
export default ProductIdContextProvider;

View File

@ -18,12 +18,8 @@ export const WebSocketContext = createContext<
const WebSocketContextProvider = ({ children }: { children: ReactNode }) => {
const [socket, setSocket] = useState<WebSocket>(null);
const [installations, setInstallations] = useState<I_Installation[]>(null);
const [installationStatus, setInstallationStatus] = useState<
Record<number, number>
>({});
const [installationMode, setInstallatioMode] = useState<
Record<number, boolean>
>({});
const [installationStatus, setInstallationStatus] = useState(new Map());
const [installationMode, setInstallationMode] = useState(new Map());
const BUFFER_LENGTH = 5;
useEffect(() => {
@ -53,22 +49,33 @@ const WebSocketContextProvider = ({ children }: { children: ReactNode }) => {
// Listen for messages
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data); // Parse the JSON data
if (message.id != -1) {
const installation_id = message.id;
//console.log('Message from server ', installation_id, status);
setInstallatioMode((prevMode) => {
// Create a new object by spreading the previous state
const newMode = { ...prevMode };
newMode[installation_id] = message.testingMode;
if (Array.isArray(message)) {
setInstallationMode((prevMode) => {
const newMode = new Map(prevMode);
message.forEach((item) => {
newMode.set(item.id, item.testingMode);
});
return newMode;
});
setInstallationStatus((prevStatus) => {
// Create a new object by spreading the previous state
const newStatus = { ...prevStatus };
newStatus[installation_id] = message.status;
const newStatus = new Map(prevStatus);
message.forEach((item) => {
newStatus.set(item.id, item.status);
});
return newStatus;
});
} else if (message.id != -1) {
const installation_id = message.id;
setInstallationMode((prevMode) => {
const newMode = new Map(prevMode);
newMode.set(message.id, message.testingMode);
return newMode;
});
setInstallationStatus((prevStatus) => {
const newStatus = new Map(prevStatus);
newStatus.set(message.id, message.status);
return newStatus;
});
}
@ -86,17 +93,16 @@ const WebSocketContextProvider = ({ children }: { children: ReactNode }) => {
};
const getStatus = (installationId: number) => {
let status;
if (!installationStatus.hasOwnProperty(installationId)) {
status = -2;
} else {
status = installationStatus[installationId];
}
return status;
return installationStatus.get(installationId);
// if (installationStatus.has(installationId)) {
// installationStatus.get(installationId);
// } else {
// return -2;
// }
};
const getTestingMode = (installationId: number) => {
return installationMode[installationId];
return installationMode.get(installationId);
};
return (

View File

@ -8,6 +8,7 @@ import { SidebarProvider } from 'src/contexts/SidebarContext';
import * as serviceWorker from 'src/serviceWorker';
import UserContextProvider from './contexts/userContext';
import TokenContextProvider from './contexts/tokenContext';
import ProductIdContextProvider from './contexts/ProductIdContextProvider';
ReactDOM.render(
<HelmetProvider>
@ -15,7 +16,9 @@ ReactDOM.render(
<BrowserRouter>
<UserContextProvider>
<TokenContextProvider>
<App />
<ProductIdContextProvider>
<App />
</ProductIdContextProvider>
</TokenContextProvider>
</UserContextProvider>
</BrowserRouter>

View File

@ -26,6 +26,8 @@ export interface overviewInterface {
pvProduction: chartInfoInterface;
dcBusVoltage: chartInfoInterface;
overview: chartInfoInterface;
ACLoad: chartInfoInterface;
DCLoad: chartInfoInterface;
}
export interface chartAggregatedDataInterface {
@ -46,6 +48,8 @@ export interface chartDataInterface {
gridPower: { name: string; data: number[] };
pvProduction: { name: string; data: number[] };
dcBusVoltage: { name: string; data: number[] };
ACLoad: { name: string; data: number[] };
DCLoad: { name: string; data: number[] };
}
export interface BatteryDataInterface {
@ -339,7 +343,9 @@ export const transformInputToDailyData = async (
'/Battery/Dc/Power',
'/GridMeter/Ac/Power/Active',
'/PvOnDc/Dc/Power',
'/DcDc/Dc/Link/Voltage'
'/DcDc/Dc/Link/Voltage',
'/LoadOnAcGrid/Power/Active',
'/LoadOnDc/Power'
];
const categories = [
'soc',
@ -347,7 +353,9 @@ export const transformInputToDailyData = async (
'dcPower',
'gridPower',
'pvProduction',
'dcBusVoltage'
'dcBusVoltage',
'ACLoad',
'DCLoad'
];
const chartData: chartDataInterface = {
@ -356,7 +364,9 @@ export const transformInputToDailyData = async (
dcPower: { name: 'Battery Power', data: [] },
gridPower: { name: 'Grid Power', data: [] },
pvProduction: { name: 'Pv Production', data: [] },
dcBusVoltage: { name: 'DC Bus Voltage', data: [] }
dcBusVoltage: { name: 'DC Bus Voltage', data: [] },
ACLoad: { name: 'AC Load', data: [] },
DCLoad: { name: 'DC Load', data: [] }
};
const chartOverview: overviewInterface = {
@ -367,7 +377,9 @@ export const transformInputToDailyData = async (
gridPower: { magnitude: 0, unit: '', min: 0, max: 0 },
pvProduction: { magnitude: 0, unit: '', min: 0, max: 0 },
dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 },
overview: { magnitude: 0, unit: '', min: 0, max: 0 }
overview: { magnitude: 0, unit: '', min: 0, max: 0 },
ACLoad: { magnitude: 0, unit: '', min: 0, max: 0 },
DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 }
};
categories.forEach((category) => {
@ -560,7 +572,9 @@ export const transformInputToAggregatedData = async (
gridPower: { magnitude: 0, unit: '', min: 0, max: 0 },
pvProduction: { magnitude: 0, unit: '', min: 0, max: 0 },
dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 },
overview: { magnitude: 0, unit: '', min: 0, max: 0 }
overview: { magnitude: 0, unit: '', min: 0, max: 0 },
ACLoad: { magnitude: 0, unit: '', min: 0, max: 0 },
DCLoad: { magnitude: 0, unit: '', min: 0, max: 0 }
};
pathsToSearch.forEach((path) => {

View File

@ -16,6 +16,7 @@ import TableChartTwoToneIcon from '@mui/icons-material/TableChartTwoTone';
import { FormattedMessage } from 'react-intl';
import { UserContext } from '../../../../contexts/userContext';
import { UserType } from '../../../../interfaces/UserTypes';
import { ProductIdContext } from '../../../../contexts/ProductIdContextProvider';
const MenuWrapper = styled(Box)(
({ theme }) => `
@ -163,6 +164,7 @@ function SidebarMenu() {
const { closeSidebar } = useContext(SidebarContext);
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const { accessToSalimax, accessToSalidomo } = useContext(ProductIdContext);
return (
<>
@ -176,23 +178,25 @@ function SidebarMenu() {
}
>
<SubMenuWrapper>
<List component="div">
<ListItem component="div">
<Button
disableRipple
component={RouterLink}
onClick={closeSidebar}
to="/installations"
startIcon={<BrightnessLowTwoToneIcon />}
>
<Box sx={{ marginTop: '3px' }}>
<FormattedMessage id="salimax" defaultMessage="Salimax" />
</Box>
</Button>
</ListItem>
</List>
{accessToSalimax && (
<List component="div">
<ListItem component="div">
<Button
disableRipple
component={RouterLink}
onClick={closeSidebar}
to="/installations"
startIcon={<BrightnessLowTwoToneIcon />}
>
<Box sx={{ marginTop: '3px' }}>
<FormattedMessage id="salimax" defaultMessage="Salimax" />
</Box>
</Button>
</ListItem>
</List>
)}
{currentUser.userType == UserType.admin && (
{accessToSalidomo && (
<List component="div">
<ListItem component="div">
<Button