Merge remote-tracking branch 'origin/main'

This commit is contained in:
atef 2024-02-23 13:08:25 +01:00
commit d500b500d9
24 changed files with 2642 additions and 1578 deletions

View File

@ -299,6 +299,7 @@ internal static class Program
{
subscribedNow = true;
_subscribeToQueueForTheFirstTime = true;
_prevSalimaxState = currentSalimaxState.Status;
_subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp);
}

View File

@ -128,8 +128,8 @@ public partial class Battery48TlRecord
{
Boolean HasBit(Int16 bit) => (_AlarmFlags & 1uL << bit) > 0;
if (HasBit(0) ) yield return "Tam : BMS temperature too low";
if (HasBit(2) ) yield return "TaM2 : BMS temperature too high";
if (HasBit(0)) yield return "Tam : BMS temperature too low";
if (HasBit(2)) yield return "TaM2 : BMS temperature too high";
if (HasBit(3) ) yield return "Tbm : Battery temperature too low";
if (HasBit(5) ) yield return "TbM2 : Battery temperature too high";
if (HasBit(7) ) yield return "VBm2 : Bus voltage too low";

View File

@ -25,8 +25,7 @@ function App() {
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const tokencontext = useContext(TokenContext);
const { token, setNewToken, removeToken } = tokencontext;
const [forgotPassword, setForgotPassword] = useState(false);
const { token, setNewToken } = tokencontext;
const navigate = useNavigate();
const searchParams = new URLSearchParams(location.search);
const username = searchParams.get('username');
@ -43,14 +42,6 @@ function App() {
}
};
const onForgotPassword = () => {
setForgotPassword(true);
};
const resetPassword = () => {
setForgotPassword(false);
};
const Loader = (Component) => (props) =>
(
<Suspense fallback={<SuspenseLoader />}>
@ -58,11 +49,6 @@ function App() {
</Suspense>
);
// Dashboards
const Installations = Loader(
lazy(() => import('src/content/dashboards/Installations/'))
);
const ResetPassword = Loader(
lazy(() => import('src/components/ResetPassword'))
);
@ -84,23 +70,9 @@ function App() {
navigate(routes.installations);
}
})
.catch((error) => {});
.catch(() => {});
};
// Status
const Status404 = Loader(
lazy(() => import('src/content/pages/Status/Status404'))
);
const Status500 = Loader(
lazy(() => import('src/content/pages/Status/Status500'))
);
const StatusComingSoon = Loader(
lazy(() => import('src/content/pages/Status/ComingSoon'))
);
const StatusMaintenance = Loader(
lazy(() => import('src/content/pages/Status/Maintenance'))
);
if (username) {
loginToResetPassword();
}
@ -174,7 +146,6 @@ function App() {
</AccessContextProvider>
}
/>
<Route path={routes.users + '*'} element={<Users />} />
<Route
path={'*'}

View File

@ -18,5 +18,7 @@
"information": "information",
"configuration": "configuration",
"login": "/login/",
"forgotPassword": "/forgotPassword/"
"forgotPassword": "/forgotPassword/",
"mainstats": "mainstats",
"general": "general"
}

View File

@ -1,37 +1,47 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
Container,
Grid,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
useTheme
TableRow
} from '@mui/material';
import { TopologyValues } from '../Log/graph.util';
import { Link } from 'react-router-dom';
import {
Link,
Route,
Routes,
useLocation,
useNavigate
} from 'react-router-dom';
import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl';
import { I_S3Credentials } from '../../../interfaces/S3Types';
import routes from '../../../Resources/routes.json';
import MainStats from './MainStats';
interface BatteryViewProps {
values: TopologyValues;
s3Credentials: I_S3Credentials;
}
function BatteryView(props: BatteryViewProps) {
if (props.values === null) {
return null;
}
const theme = useTheme();
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
const currentTab = searchParams.get('tab');
const currentPath = useLocation();
const navigate = useNavigate();
const [currentTab, setCurrentTab] = useState<string>(undefined);
const numOfBatteries = props.values.batteryView.values[0].value
.toString()
.split(',').length;
const batteryData = [];
let batteryId = 1;
// Use a for loop to generate battery data
for (let index = 1; index <= numOfBatteries * 7; index += 7) {
const battery = {
@ -61,8 +71,68 @@ function BatteryView(props: BatteryViewProps) {
batteryData.push(battery);
}
const handleMainStatsButton = () => {
navigate(routes.mainstats);
};
useEffect(() => {
let path = currentPath.pathname.split('/');
setCurrentTab(path[path.length - 1]);
}, [currentPath]);
return (
<TableContainer component={Paper}>
<>
<Container maxWidth="xl">
<Grid container>
<Routes>
<Route
path={routes.mainstats}
element={
<MainStats s3Credentials={props.s3Credentials}></MainStats>
}
/>
</Routes>
{currentTab === 'batteryview' && (
<Grid item xs={6} md={6}>
<Button
variant="contained"
sx={{
marginTop: '20px',
backgroundColor: '#808080',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage
id="main_stats"
defaultMessage="Battery View"
/>
</Button>
<Button
variant="contained"
onClick={handleMainStatsButton}
sx={{
marginTop: '20px',
marginLeft: '20px',
// backgroundColor: mainStatsData ? '#808080' : '#ffc04d',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="main_stats" defaultMessage="Main Stats" />
</Button>
</Grid>
)}
</Grid>
{currentTab === 'batteryview' && (
<TableContainer
component={Paper}
sx={{ marginTop: '20px', marginBottom: '20px' }}
>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
@ -142,7 +212,9 @@ function BatteryView(props: BatteryViewProps) {
width: '10%',
textAlign: 'center',
backgroundColor:
battery.AverageTemperature > 270 ? '#FF033E' : '#32CD32 '
battery.AverageTemperature > 270
? '#FF033E'
: '#32CD32 '
}}
>
{battery.AverageTemperature}
@ -153,7 +225,8 @@ function BatteryView(props: BatteryViewProps) {
width: '20%',
textAlign: 'center',
padding: '8px',
fontWeight: battery.Warnings !== '' ? 'bold' : 'inherit',
fontWeight:
battery.Warnings !== '' ? 'bold' : 'inherit',
backgroundColor:
battery.Warnings === '' ? 'inherit' : '#ff9900',
color: battery.Warnings != '' ? 'black' : 'inherit'
@ -165,10 +238,12 @@ function BatteryView(props: BatteryViewProps) {
<Link
style={{ color: 'black' }}
to={
'?installation=' +
installationId +
'&tab=log' +
'&open=warning'
currentPath.pathname.substring(
0,
currentPath.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=warning'
}
>
Multiple Warnings
@ -193,10 +268,12 @@ function BatteryView(props: BatteryViewProps) {
<Link
style={{ color: 'black' }}
to={
'?installation=' +
installationId +
'&tab=log' +
'&open=error'
currentPath.pathname.substring(
0,
currentPath.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=error'
}
>
Multiple Alarms
@ -210,6 +287,9 @@ function BatteryView(props: BatteryViewProps) {
</TableBody>
</Table>
</TableContainer>
)}
</Container>
</>
);
}

View File

@ -0,0 +1,772 @@
import { Box, Card, Container, Grid, Modal, Typography } from '@mui/material';
import { FormattedMessage } from 'react-intl';
import React, { useEffect, useState } from 'react';
import { I_S3Credentials } from '../../../interfaces/S3Types';
import ReactApexChart from 'react-apexcharts';
import { getChartOptions } from '../Overview/chartOptions';
import {
BatteryDataInterface,
BatteryOverviewInterface,
transformInputToBatteryViewData
} from '../../../interfaces/Chart';
import dayjs from 'dayjs';
import { TimeSpan, UnixTime } from '../../../dataCache/time';
import Button from '@mui/material/Button';
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import CircularProgress from '@mui/material/CircularProgress';
import { useLocation, useNavigate } from 'react-router-dom';
interface MainStatsProps {
s3Credentials: I_S3Credentials;
}
function MainStats(props: MainStatsProps) {
const [chartState, setChartState] = useState(0);
const [batteryViewDataArray, setBatteryViewDataArray] = useState<
{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}[]
>([]);
const [isDateModalOpen, setIsDateModalOpen] = useState(false);
const [dateOpen, setDateOpen] = useState(false);
const navigate = useNavigate();
const [startDate, setStartDate] = useState(dayjs().add(-1, 'day'));
const [endDate, setEndDate] = useState(dayjs());
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
const [dateSelectionError, setDateSelectionError] = useState('');
const [loading, setLoading] = useState(true);
const location = useLocation();
const blueColors = [
'#99CCFF',
'#80BFFF',
'#6699CC',
'#4D99FF',
'#2670E6',
'#3366CC',
'#1A4D99',
'#133366',
'#0D274D',
'#081A33'
];
const redColors = [
'#ff9090',
'#ff7070',
'#ff3f3f',
'#ff1e1e',
'#ff0606',
'#fc0000',
'#f40000',
'#d40000',
'#a30000',
'#7a0000'
];
const orangeColors = [
'#ffdb99',
'#ffc968',
'#ffb837',
'#ffac16',
'#ffa706',
'#FF8C00',
'#d48900',
'#CC7A00',
'#a36900',
'#993D00'
];
useEffect(() => {
setLoading(true);
const resultPromise: Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewData(
props.s3Credentials,
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)).start,
UnixTime.now()
);
resultPromise
.then((result) => {
setBatteryViewDataArray((prevData) =>
prevData.concat({
chartData: result.chartData,
chartOverview: result.chartOverview
})
);
setLoading(false);
})
.catch((error) => {
console.error('Error:', error);
});
}, []);
function generateSeries(chartData, category, color) {
const series = [];
const pathsToSearch = [
'Battery1',
'Battery2',
'Battery3',
'Battery4',
'Battery5',
'Battery6',
'Battery7',
'Battery8',
'Battery9',
'Battery10'
];
let i = 0;
// Assuming the chartData.Soc.data structure
pathsToSearch.forEach((devicePath) => {
if (
Object.hasOwnProperty.call(chartData[category].data, devicePath) &&
chartData[category].data[devicePath].data.length != 0
) {
series.push({
...chartData[category].data[devicePath],
color:
color === 'blue'
? blueColors[i]
: color === 'red'
? redColors[i]
: orangeColors[i]
});
}
i++;
});
return series;
}
const handleCancel = () => {
setIsDateModalOpen(false);
setDateOpen(false);
};
const handleConfirm = () => {
setIsDateModalOpen(false);
setDateOpen(false);
if (endDate.isAfter(dayjs())) {
setDateSelectionError('You cannot ask for future data');
setErrorDateModalOpen(true);
return;
} else if (startDate.isAfter(endDate)) {
setDateSelectionError('Εnd date must precede start date');
setErrorDateModalOpen(true);
return;
}
setLoading(true);
const resultPromise: Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewData(
props.s3Credentials,
UnixTime.fromTicks(startDate.unix()),
UnixTime.fromTicks(endDate.unix())
);
resultPromise
.then((result) => {
setBatteryViewDataArray((prevData) =>
prevData.concat({
chartData: result.chartData,
chartOverview: result.chartOverview
})
);
setLoading(false);
setChartState(batteryViewDataArray.length);
})
.catch((error) => {
console.error('Error:', error);
});
};
const handleSetDate = () => {
setDateOpen(true);
setIsDateModalOpen(true);
};
const handleBatteryViewButton = () => {
navigate(
location.pathname.split('/').slice(0, -2).join('/') + '/batteryview'
);
};
const handleGoBack = () => {
if (chartState > 0) {
setChartState(chartState - 1);
}
};
const handleGoForward = () => {
if (chartState + 1 < batteryViewDataArray.length) {
setChartState(chartState + 1);
}
};
const handleOkOnErrorDateModal = () => {
setErrorDateModalOpen(false);
};
const handleBeforeZoom = (chartContext, { xaxis }) => {
const startX = parseInt(xaxis.min) / 1000;
const endX = parseInt(xaxis.max) / 1000;
setLoading(true);
const resultPromise: Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewData(
props.s3Credentials,
UnixTime.fromTicks(startX),
UnixTime.fromTicks(endX)
);
resultPromise
.then((result) => {
setBatteryViewDataArray((prevData) =>
prevData.concat({
chartData: result.chartData,
chartOverview: result.chartOverview
})
);
setLoading(false);
setChartState(batteryViewDataArray.length);
})
.catch((error) => {
console.error('Error:', error);
});
};
return (
<>
{loading && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography variant="body2" style={{ color: 'black' }} mt={2}>
Fetching data...
</Typography>
</Container>
)}
{isErrorDateModalOpen && (
<Modal open={isErrorDateModalOpen} onClose={() => {}}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 450,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography
variant="body1"
gutterBottom
sx={{ fontWeight: 'bold' }}
>
{dateSelectionError}
</Typography>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={handleOkOnErrorDateModal}
>
Ok
</Button>
</Box>
</Modal>
)}
{isDateModalOpen && (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Modal open={isDateModalOpen} onClose={() => {}}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 450,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<DateTimePicker
label="Select Start Date"
value={startDate}
onChange={(newDate) => setStartDate(newDate)}
sx={{
marginTop: 2
}}
/>
<DateTimePicker
label="Select End Date"
value={endDate}
onChange={(newDate) => setEndDate(newDate)}
sx={{
marginTop: 2
}}
/>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={handleConfirm}
>
Confirm
</Button>
<Button
sx={{
marginTop: 2,
marginLeft: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={handleCancel}
>
Cancel
</Button>
</div>
</Box>
</Modal>
</LocalizationProvider>
)}
{!loading && (
<>
{' '}
<Grid item xs={6} md={6}>
<Button
variant="contained"
onClick={handleBatteryViewButton}
sx={{
marginTop: '20px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="main_stats" defaultMessage="Battery View" />
</Button>
<Button
variant="contained"
sx={{
marginTop: '20px',
marginLeft: '20px',
backgroundColor: '#808080',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="main_stats" defaultMessage="Main Stats" />
</Button>
<Button
variant="contained"
onClick={handleSetDate}
disabled={loading}
sx={{
marginTop: '20px',
marginLeft: '20px',
backgroundColor: dateOpen ? '#808080' : '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="set_date" defaultMessage="Set Date" />
</Button>
</Grid>
<Grid
container
justifyContent="flex-end"
alignItems="center"
item
xs={6}
md={6}
>
<Button
variant="contained"
disabled={!(chartState > 0)}
onClick={handleGoBack}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="goback" defaultMessage="Zoom out" />
</Button>
<Button
variant="contained"
disabled={!(chartState < batteryViewDataArray.length - 1)}
onClick={handleGoForward}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="goback" defaultMessage="Zoom in" />
</Button>
</Grid>
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item md={12} 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="battery_soc"
defaultMessage="Battery SOC (State Of Charge)"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
<ReactApexChart
options={{
...getChartOptions(
batteryViewDataArray[chartState].chartOverview.Soc,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={generateSeries(
batteryViewDataArray[chartState].chartData,
'Soc',
'blue'
)}
type="line"
height={420}
/>
</Card>
</Grid>
<Grid item md={12} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '10px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="battery_soc"
defaultMessage="Battery Temperature"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
<ReactApexChart
options={{
...getChartOptions(
batteryViewDataArray[chartState].chartOverview
.Temperature,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={generateSeries(
batteryViewDataArray[chartState].chartData,
'Temperature',
'blue'
)}
type="line"
height={420}
/>
</Card>
</Grid>
<Grid item md={12} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '10px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="battery_power"
defaultMessage="Battery Power"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
<ReactApexChart
options={{
...getChartOptions(
batteryViewDataArray[chartState].chartOverview.Power,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={generateSeries(
batteryViewDataArray[chartState].chartData,
'Power',
'red'
)}
type="line"
height={420}
/>
</Card>
</Grid>
<Grid item md={12} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '10px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="battery_voltage"
defaultMessage="Battery Voltage"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
<ReactApexChart
options={{
...getChartOptions(
batteryViewDataArray[chartState].chartOverview.Voltage,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={generateSeries(
batteryViewDataArray[chartState].chartData,
'Voltage',
'orange'
)}
type="line"
height={420}
/>
</Card>
</Grid>
<Grid item md={12} xs={12}>
<Card
sx={{
overflow: 'visible',
marginTop: '10px',
marginBottom: '30px'
}}
>
<Box
sx={{
marginLeft: '20px'
}}
>
<Box display="flex" alignItems="center">
<Box>
<Typography variant="subtitle1" noWrap>
<FormattedMessage
id="battery_current"
defaultMessage="Battery Current"
/>
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
pt: 3
}}
></Box>
</Box>
<ReactApexChart
options={{
...getChartOptions(
batteryViewDataArray[chartState].chartOverview.Current,
'daily',
[],
true
),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={generateSeries(
batteryViewDataArray[chartState].chartData,
'Current',
'orange'
)}
type="line"
height={420}
/>
</Card>
</Grid>
</Grid>
</>
)}
</>
);
}
export default MainStats;

View File

@ -0,0 +1,421 @@
import {
Alert,
Box,
CardContent,
CircularProgress,
Container,
Grid,
IconButton,
Modal,
TextField,
Typography,
useTheme
} from '@mui/material';
import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material';
import React, { useContext, useState } from 'react';
import { I_S3Credentials } from '../../../interfaces/S3Types';
import { I_Installation } from '../../../interfaces/InstallationTypes';
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
import { UserContext } from '../../../contexts/userContext';
interface InformationProps {
values: I_Installation;
s3Credentials: I_S3Credentials;
type?: string;
}
function Information(props: InformationProps) {
if (props.values === null) {
return null;
}
const context = useContext(UserContext);
const { currentUser } = context;
const theme = useTheme();
const [formValues, setFormValues] = useState(props.values);
const requiredFields = ['name', 'region', 'location', 'country'];
const installationContext = useContext(InstallationsContext);
const {
updateInstallation,
loading,
setLoading,
error,
setError,
updated,
setUpdated,
deleteInstallation
} = installationContext;
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSubmit = () => {
setLoading(true);
setError(false);
updateInstallation(formValues, props.type);
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!formValues[field]) {
return false;
}
}
return true;
};
const [openModalDeleteInstallation, setOpenModalDeleteInstallation] =
useState(false);
const handleDelete = () => {
setLoading(true);
setError(false);
setOpenModalDeleteInstallation(true);
};
const deleteInstallationModalHandle = () => {
setOpenModalDeleteInstallation(false);
deleteInstallation(formValues, props.type);
setLoading(false);
};
const deleteInstallationModalHandleCancel = () => {
setOpenModalDeleteInstallation(false);
setLoading(false);
};
return (
<>
{openModalDeleteInstallation && (
<Modal
open={openModalDeleteInstallation}
onClose={deleteInstallationModalHandleCancel}
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 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}
>
Delete
</Button>
<Button
sx={{
marginTop: 2,
marginLeft: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={deleteInstallationModalHandleCancel}
>
Cancel
</Button>
</div>
</Box>
</Modal>
)}
<Container maxWidth="xl">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12} md={12}>
<CardContent>
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '50ch' }
}}
noValidate
autoComplete="off"
>
<div>
<TextField
label={
<FormattedMessage
id="customerName"
defaultMessage="Customer Name"
/>
}
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.region === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="location"
defaultMessage="Location"
/>
}
name="location"
value={formValues.location}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.location === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage id="country" defaultMessage="Country" />
}
name="country"
value={formValues.country}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.country === ''}
/>
</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>
{currentUser.hasWriteAccess && (
<>
<div>
<TextField
label="Vpn IP"
name="VpnIp"
value={formValues.vpnIp}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label="S3 Write Key"
name="s3writekey"
value={formValues.s3WriteKey}
variant="outlined"
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
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>
</Container>
</>
);
}
export default Information;

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useState } from 'react';
import {
Card,
CircularProgress,
@ -13,11 +13,11 @@ import {
useTheme
} from '@mui/material';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import Installation from './Installation';
import CancelIcon from '@mui/icons-material/Cancel';
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
import { FormattedMessage } from 'react-intl';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import routes from '../../../Resources/routes.json';
interface FlatInstallationViewProps {
installations: I_Installation[];
@ -28,31 +28,32 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
const webSocketContext = useContext(WebSocketContext);
const { getStatus } = webSocketContext;
const navigate = useNavigate();
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
const currentLocation = useLocation();
const handleSelectOneInstallation = (installationID: number): void => {
if (selectedInstallation != installationID) {
setSelectedInstallation(installationID);
navigate(`?installation=${installationID}`, {
setSelectedInstallation(-1);
navigate(
routes.installations +
routes.list +
routes.installation +
`${installationID}` +
'/' +
routes.live,
{
replace: true
});
}
);
} else {
setSelectedInstallation(-1);
}
};
useEffect(() => {
setSelectedInstallation(installationId);
}, [installationId]);
const theme = useTheme();
const findInstallation = (id: number) => {
return props.installations.find((installation) => installation.id === id);
};
const handleRowMouseEnter = (id: number) => {
setHoveredRow(id);
};
@ -63,7 +64,16 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
return (
<Grid container spacing={1} sx={{ marginTop: 0.1 }}>
<Grid item sx={{ display: !installationId ? 'block' : 'none' }}>
<Grid
item
sx={{
display:
currentLocation.pathname === routes.installations + 'list' ||
currentLocation.pathname === routes.installations + routes.list
? 'block'
: 'none'
}}
>
<Card>
<TableContainer>
<Table>
@ -225,14 +235,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
</TableContainer>
</Card>
</Grid>
{props.installations.map((installation) => (
<Installation
key={installation.id}
current_installation={findInstallation(installation.id)}
type="installation"
></Installation>
))}
</Grid>
);
};

View File

@ -1,24 +1,7 @@
import React, { useContext, useEffect, useState } from 'react';
import {
Alert,
Box,
Card,
CardContent,
CircularProgress,
Container,
Grid,
IconButton,
Modal,
TextField,
Typography,
useTheme
} from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material';
import { Card, CircularProgress, Grid, Typography } from '@mui/material';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import Button from '@mui/material/Button';
import { TokenContext } from 'src/contexts/tokenContext';
import { UserContext } from 'src/contexts/userContext';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import AccessContextProvider from 'src/contexts/AccessContextProvider';
import Access from '../ManageAccess/Access';
import Log from 'src/content/dashboards/Log/Log';
@ -36,6 +19,9 @@ import Configuration from '../Configuration/Configuration';
import { fetchData } from 'src/content/dashboards/Installations/fetchData';
import CancelIcon from '@mui/icons-material/Cancel';
import BatteryView from '../BatteryView/BatteryView';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import routes from '../../../Resources/routes.json';
import Information from '../Information/Information';
interface singleInstallationProps {
current_installation?: I_Installation;
@ -43,80 +29,20 @@ interface singleInstallationProps {
}
function Installation(props: singleInstallationProps) {
const theme = useTheme();
const [formValues, setFormValues] = useState(props.current_installation);
const requiredFields = ['name', 'region', 'location', 'country'];
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const installationContext = useContext(InstallationsContext);
const {
updateInstallation,
loading,
setLoading,
error,
setError,
updated,
setUpdated,
deleteInstallation
} = installationContext;
const { currentUser } = context;
const location = useLocation();
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
const webSocketsContext = useContext(WebSocketContext);
const { getStatus } = webSocketsContext;
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
const currentTab = searchParams.get('tab');
const [currentTab, setCurrentTab] = useState<string>(undefined);
const [values, setValues] = useState<TopologyValues | null>(null);
const [openModalDeleteInstallation, setOpenModalDeleteInstallation] =
useState(false);
const status = getStatus(props.current_installation.id);
if (formValues == undefined) {
if (props.current_installation == undefined) {
return null;
}
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSubmit = (e) => {
setLoading(true);
setError(false);
updateInstallation(formValues, props.type);
};
const handleDelete = (e) => {
setLoading(true);
setError(false);
setOpenModalDeleteInstallation(true);
};
const deleteInstallationModalHandle = (e) => {
setOpenModalDeleteInstallation(false);
deleteInstallation(formValues, props.type);
setLoading(false);
};
const deleteInstallationModalHandleCancel = (e) => {
setOpenModalDeleteInstallation(false);
setLoading(false);
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!formValues[field]) {
return false;
}
}
return true;
};
const S3data = {
s3Region: props.current_installation.s3Region,
s3Provider: props.current_installation.s3Provider,
@ -132,15 +58,10 @@ function Installation(props: singleInstallationProps) {
const fetchDataPeriodically = async () => {
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
const date = now.toDate();
try {
const res = await fetchData(now, s3Credentials);
// if (!isMounted) {
// return false;
// }
if (res != FetchResult.notAvailable && res != FetchResult.tryLater) {
setValues(
extractValues({
@ -166,18 +87,22 @@ function Installation(props: singleInstallationProps) {
}
};
useEffect(() => {
let path = location.pathname.split('/');
setCurrentTab(path[path.length - 1]);
}, [location]);
useEffect(() => {
if (
installationId == props.current_installation.id &&
(currentTab == 'live' ||
currentTab == 'live' ||
currentTab == 'configuration' ||
currentTab == 'batteryview')
currentTab == 'batteryview'
) {
//let isMounted = true;
setFormValues(props.current_installation);
var interval;
if (currentTab == 'live' || currentTab == 'batteryview') {
fetchDataPeriodically();
interval = setInterval(fetchDataPeriodically, 2000);
}
if (currentTab == 'configuration') {
@ -186,85 +111,15 @@ function Installation(props: singleInstallationProps) {
// Cleanup function to cancel interval and update isMounted when unmounted
return () => {
//isMounted = false;
if (currentTab == 'live' || currentTab == 'batteryview') {
clearInterval(interval);
}
};
}
}, [installationId, currentTab]);
}, [currentTab, location.pathname]);
if (installationId == props.current_installation.id) {
return (
<>
{openModalDeleteInstallation && (
<Modal
open={openModalDeleteInstallation}
onClose={deleteInstallationModalHandleCancel}
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 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}
>
Delete
</Button>
<Button
sx={{
marginTop: 2,
marginLeft: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={deleteInstallationModalHandleCancel}
>
Cancel
</Button>
</div>
</Box>
</Modal>
)}
<Grid item xs={12} md={12}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Typography
@ -397,304 +252,83 @@ function Installation(props: singleInstallationProps) {
alignItems="stretch"
spacing={0}
>
{currentTab === 'information' && (
<Container maxWidth="xl">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12} md={12}>
<CardContent>
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '50ch' }
}}
noValidate
autoComplete="off"
>
<div>
<TextField
label={
<FormattedMessage
id="customerName"
defaultMessage="Customer Name"
/>
<Routes>
<Route
path={routes.information}
element={
<Information
values={props.current_installation}
s3Credentials={s3Credentials}
type={props.type}
></Information>
}
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>
<div>
<TextField
label={
<FormattedMessage
id="installation_name"
defaultMessage="Installation Name"
/>
}
name="installationName"
value={formValues.installationName}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
{currentUser.hasWriteAccess && (
<>
<div>
<TextField
label="Vpn IP"
name="VpnIp"
value={formValues.vpnIp}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label="S3 Write Key"
name="s3writekey"
value={formValues.s3WriteKey}
variant="outlined"
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
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>
</Container>
)}
{currentTab === 'overview' && (
<Overview s3Credentials={s3Credentials}></Overview>
)}
{currentTab === 'batteryview' && (
<BatteryView values={values}></BatteryView>
)}
{currentTab === 'configuration' && currentUser.hasWriteAccess && (
<Configuration
<Route
path={routes.batteryview + '*'}
element={
<BatteryView
values={values}
id={installationId}
></Configuration>
)}
{currentTab === 'manage' && currentUser.hasWriteAccess && (
<AccessContextProvider>
<Access
currentResource={formValues}
resourceType={props.type}
></Access>
</AccessContextProvider>
)}
{currentTab === 'live' && <Topology values={values}></Topology>}
{currentTab === 'log' && (
s3Credentials={s3Credentials}
></BatteryView>
}
/>
<Route
path={routes.overview}
element={<Overview s3Credentials={s3Credentials}></Overview>}
/>
<Route
path={routes.live}
element={<Topology values={values}></Topology>}
/>
<Route
path={routes.log}
element={
<Log
errorLoadingS3Data={errorLoadingS3Data}
id={props.current_installation.id}
></Log>
}
/>
{currentUser.hasWriteAccess && (
<Route
path={routes.configuration}
element={
<Configuration
values={values}
id={props.current_installation.id}
></Configuration>
}
/>
)}
{currentUser.hasWriteAccess && (
<Route
path={routes.manage}
element={
<AccessContextProvider>
<Access
currentResource={props.current_installation}
resourceType={props.type}
></Access>
</AccessContextProvider>
}
/>
)}
<Route
path={'*'}
element={<Navigate to={routes.live}></Navigate>}
/>
</Routes>
</Grid>
</Card>
</Grid>
</>
);
} else {
return null;
}
}
export default Installation;

View File

@ -1,25 +1,19 @@
import React, { useEffect, useState } from 'react';
import {
FormControl,
Grid,
InputAdornment,
TextField,
useTheme
} from '@mui/material';
import { FormControl, Grid, InputAdornment, TextField } from '@mui/material';
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
import FlatInstallationView from 'src/content/dashboards/Installations/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';
interface installationSearchProps {
installations: I_Installation[];
}
function InstallationSearch(props: installationSearchProps) {
const theme = useTheme();
const [searchTerm, setSearchTerm] = useState('');
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
const currentLocation = useLocation();
const [filteredData, setFilteredData] = useState(props.installations);
useEffect(() => {
@ -38,7 +32,13 @@ function InstallationSearch(props: installationSearchProps) {
item
xs={12}
md={6}
sx={{ display: !installationId ? 'block' : 'none' }}
sx={{
display:
currentLocation.pathname === routes.installations + 'list' ||
currentLocation.pathname === routes.installations + routes.list
? 'block'
: 'none'
}}
>
<FormControl variant="outlined">
<TextField
@ -59,6 +59,23 @@ function InstallationSearch(props: installationSearchProps) {
</Grid>
<FlatInstallationView installations={filteredData} />
<Routes>
{filteredData.map((installation) => {
return (
<Route
key={installation.id}
path={routes.installation + installation.id + '*'}
element={
<Installation
key={installation.id}
current_installation={installation}
type="installation"
></Installation>
}
/>
);
})}
</Routes>
</>
);
}

View File

@ -1,16 +1,10 @@
import React, { ChangeEvent, useContext, useEffect, useState } from 'react';
import Footer from 'src/components/Footer';
import { Box, Card, Container, Grid, Tab, Tabs, useTheme } from '@mui/material';
import { Box, Card, Container, Grid, Tab, Tabs } from '@mui/material';
import ListIcon from '@mui/icons-material/List';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import {
Link,
Route,
Routes,
useLocation,
useNavigate
} from 'react-router-dom';
import { Link, Navigate, Route, Routes, useLocation } from 'react-router-dom';
import TreeView from '../Tree/treeView';
import routes from 'src/Resources/routes.json';
import InstallationSearch from './InstallationSearch';
@ -21,23 +15,42 @@ import Installation from './Installation';
import { WebSocketContext } from '../../../contexts/WebSocketContextProvider';
function InstallationTabs() {
const theme = useTheme();
const location = useLocation();
const navigate = useNavigate();
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
//const currentTab = searchParams.get('tab');
const [singleInstallationID, setSingleInstallationID] = useState(-1);
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const [currentTab, setCurrentTab] = useState<string>(searchParams.get('tab'));
const { currentUser } = context;
const tabList = [
'live',
'overview',
'manage',
'batteryview',
'log',
'information',
'configuration'
];
const [currentTab, setCurrentTab] = useState<string>(undefined);
const { installations, fetchAllInstallations } =
useContext(InstallationsContext);
const webSocketsContext = useContext(WebSocketContext);
const { socket, openSocket } = webSocketsContext;
useEffect(() => {
let path = location.pathname.split('/');
if (path[path.length - 2] === 'list') {
setCurrentTab('list');
} else if (path[path.length - 2] === 'tree') {
setCurrentTab('tree');
} else {
setCurrentTab(
tabList.includes(path[path.length - 1])
? path[path.length - 1]
: undefined
);
}
}, [location]);
useEffect(() => {
if (!socket && installations.length > 0) {
openSocket(installations);
@ -48,51 +61,7 @@ function InstallationTabs() {
if (installations.length === 0) {
fetchAllInstallations();
}
if (installations.length === 1) {
if (!currentTab) {
navigate(`?installation=${installations[0].id}&tab=live`, {
replace: true
});
setCurrentTab('live');
} else {
navigate(`?installation=${installations[0].id}&tab=${currentTab}`, {
replace: true
});
}
} else if (installations.length > 1) {
if (
location.pathname === '/installations' ||
location.pathname === '/installations/'
) {
navigate(routes.installations + routes.list, {
replace: true
});
} else if (
location.pathname === '/installations/tree/' &&
!installationId
) {
setCurrentTab('tree');
} else if (
location.pathname === '/installations/list/' &&
!installationId
) {
setCurrentTab('list');
}
if (installationId) {
if (currentTab == 'list' || currentTab == 'tree') {
navigate(`?installation=${installationId}&tab=live`, {
replace: true
});
setCurrentTab('live');
} else {
navigate(`?installation=${installationId}&tab=${currentTab}`, {
replace: true
});
}
}
}
}, [location.pathname, navigate, installationId, installations]);
}, [installations]);
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value);
@ -164,7 +133,10 @@ function InstallationTabs() {
}
];
const tabs = installationId
const tabs =
currentTab != 'list' &&
currentTab != 'tree' &&
!location.pathname.includes('folder')
? currentUser.hasWriteAccess
? [
{
@ -181,7 +153,9 @@ function InstallationTabs() {
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
label: (
<FormattedMessage id="overview" defaultMessage="Overview" />
)
},
{
value: 'batteryview',
@ -208,7 +182,10 @@ function InstallationTabs() {
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
<FormattedMessage
id="information"
defaultMessage="Information"
/>
)
},
@ -238,7 +215,9 @@ function InstallationTabs() {
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
label: (
<FormattedMessage id="overview" defaultMessage="Overview" />
)
},
{
value: 'batteryview',
@ -256,7 +235,10 @@ function InstallationTabs() {
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
<FormattedMessage
id="information"
defaultMessage="Information"
/>
)
}
]
@ -293,7 +275,10 @@ function InstallationTabs() {
to={
tab.value === 'list' || tab.value === 'tree'
? routes[tab.value]
: `?installation=${installationId}&tab=${routes[tab.value]}`
: location.pathname.substring(
0,
location.pathname.lastIndexOf('/') + 1
) + routes[tab.value]
}
/>
))}
@ -319,6 +304,12 @@ function InstallationTabs() {
}
/>
<Route path={routes.tree + '*'} element={<TreeView />} />
<Route
path={'*'}
element={
<Navigate to={routes.installations + routes.list}></Navigate>
}
></Route>
</Routes>
</Grid>
</Card>
@ -343,9 +334,12 @@ function InstallationTabs() {
value={tab.value}
component={Link}
label={tab.label}
to={`?installation=${installations[0].id}&tab=${
routes[tab.value]
}`}
to={
location.pathname.substring(
0,
location.pathname.lastIndexOf('/') + 1
) + routes[tab.value]
}
/>
))}
</Tabs>

View File

@ -1,5 +1,4 @@
import { DataPoint, DataRecord } from 'src/dataCache/data';
import { TimeRange, UnixTime } from 'src/dataCache/time';
export interface I_CsvEntry {
value: string | number;
@ -100,9 +99,9 @@ export const topologyPaths: TopologyPaths = {
gridToAcInConnection: ['/GridMeter/Ac/Power/Active'],
gridBus: [
'/GridMeter/Ac/L1/Power/Active',
'/GridMeter/Ac/L2/Power/Active',
'/GridMeter/Ac/L3/Power/Active'
'/GridMeter/Ac/L1/Voltage',
'/GridMeter/Ac/L2/Voltage',
'/GridMeter/Ac/L3/Voltage'
],
gridBusToPvOnGridbusConnection: ['/PvOnAcGrid/Power/Active'],
@ -110,19 +109,19 @@ export const topologyPaths: TopologyPaths = {
gridBusToIslandBusConnection: ['/AcGridToAcIsland/Power/Active'],
islandBus: [
'/AcDc/Ac/L1/Power/Active',
'/AcDc/Ac/L2/Power/Active',
'/AcDc/Ac/L3/Power/Active'
'/AcDc/Ac/L1/Voltage',
'/AcDc/Ac/L2/Voltage',
'/AcDc/Ac/L3/Voltage'
],
islandBusToLoadOnIslandBusConnection: ['/LoadOnAcIsland/Ac/Power/Active'],
islandBusToInverter: ['/AcDc/Dc/Power'],
pvOnIslandBusToIslandBusConnection: ['/PvOnAcIsland/Power/Active'],
inverter: [
'/AcDc/Devices/1/Status/Ac/Power/Active',
'/AcDc/Devices/2/Status/Ac/Power/Active',
'/AcDc/Devices/3/Status/Ac/Power/Active',
'/AcDc/Devices/4/Status/Ac/Power/Active'
'/AcDc/Ac/L1/Power/Active',
'/AcDc/Ac/L2/Power/Active',
'/AcDc/Ac/L3/Power/Active',
'/AcDc/Ac/L4/Power/Active'
],
inverterToDcBus: ['/AcDcToDcLink/Power'],
@ -256,7 +255,9 @@ export const extractValues = (
for (const path of paths) {
if (timeSeriesData.value.hasOwnProperty(path)) {
topologyValues.push({
unit: timeSeriesData.value[path].unit,
unit: timeSeriesData.value[path].unit.includes('~')
? timeSeriesData.value[path].unit.replace('~', '')
: timeSeriesData.value[path].unit,
value: timeSeriesData.value[path].value
});
}
@ -302,19 +303,3 @@ export const getAmount = (
(Math.abs(values[0].value as number) / highestConnectionValue).toFixed(1)
);
};
export const createTimes = (
range: TimeRange,
numberOfNodes: number
): UnixTime[] => {
const oneSpan = range.duration.divide(numberOfNodes);
//console.log(oneSpan);
const roundedRange = TimeRange.fromTimes(
range.start.round(oneSpan),
range.end.round(oneSpan)
);
const unixTimes = range.sample(oneSpan);
return unixTimes;
};

View File

@ -167,7 +167,7 @@ export const getChartOptions = (
style: {
fontSize: '12px'
},
offsetY: -190,
offsetY: -185,
offsetX: 25,
rotate: 0
},
@ -182,6 +182,7 @@ export const getChartOptions = (
},
tooltip: {
shared: true,
x: {
format: 'dd MMM HH:mm:ss'
},

View File

@ -41,7 +41,7 @@ const computeLast7Days = (): string[] => {
function Overview(props: OverviewProps) {
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const { currentUser } = context;
const [dailyData, setDailyData] = useState(true);
const [weeklyData, setWeeklyData] = useState(false);
@ -444,20 +444,20 @@ function Overview(props: OverviewProps) {
>
<FormattedMessage id="lastweek" defaultMessage="Last week" />
</Button>
<Button
variant="contained"
onClick={handleMonthData}
disabled={loading}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: monthlyData ? '#808080' : '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="lastmonth" defaultMessage="Last Month" />
</Button>
{/*<Button*/}
{/* variant="contained"*/}
{/* onClick={handleMonthData}*/}
{/* disabled={loading}*/}
{/* sx={{*/}
{/* marginTop: '20px',*/}
{/* marginLeft: '10px',*/}
{/* backgroundColor: monthlyData ? '#808080' : '#ffc04d',*/}
{/* color: '#000000',*/}
{/* '&:hover': { bgcolor: '#f7b34d' }*/}
{/* }}*/}
{/*>*/}
{/* <FormattedMessage id="lastmonth" defaultMessage="Last Month" />*/}
{/*</Button>*/}
{dailyData && (
<>
<Button

View File

@ -57,17 +57,6 @@ function Topology(props: TopologyProps) {
}}
/>
</div>
{/*<Switch*/}
{/* edge="start"*/}
{/* color="secondary"*/}
{/* onChange={handleSwitch()}*/}
{/* sx={{*/}
{/* '& .MuiSwitch-thumb': {*/}
{/* backgroundColor: 'orange'*/}
{/* }*/}
{/* }}*/}
{/*/>*/}
</Grid>
<Grid

View File

@ -47,7 +47,7 @@ function formatPower(value, unit) {
magnitude++;
}
const roundedValue = Math.round(value);
const roundedValue = value.toFixed(1);
//Filter all values less than 100 Watts
if (magnitude === 0 && value < 100 && unit === 'W') {

View File

@ -45,7 +45,7 @@ function formatPower(value) {
magnitude++;
}
const roundedValue = Math.round(value);
const roundedValue = value.toFixed(1);
//Filter all values less than 100 Watts
if (magnitude === 0 && value < 100) {

View File

@ -9,7 +9,7 @@ import { makeStyles } from '@mui/styles';
import CancelIcon from '@mui/icons-material/Cancel';
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
import routes from 'src/Resources/routes.json';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
interface CustomTreeItemProps {
node: I_Installation | I_Folder;
@ -43,8 +43,7 @@ function CustomTreeItem(props: CustomTreeItemProps) {
const status = getStatus(props.node.id);
const navigate = useNavigate();
const [selected, setSelected] = useState(false);
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
const currentLocation = useLocation();
const handleSelectOneInstallation = (): void => {
let installation = props.node;
@ -52,8 +51,10 @@ function CustomTreeItem(props: CustomTreeItemProps) {
navigate(
routes.installations +
routes.tree +
'?installation=' +
installation.id.toString(),
routes.installation +
installation.id +
'/' +
routes.live,
{
replace: true
}
@ -63,8 +64,10 @@ function CustomTreeItem(props: CustomTreeItemProps) {
navigate(
routes.installations +
routes.tree +
'?folder=' +
installation.id.toString(),
routes.folder +
installation.id +
'/' +
routes.information,
{
replace: true
}
@ -152,7 +155,12 @@ function CustomTreeItem(props: CustomTreeItemProps) {
</div>
}
sx={{
display: !installationId ? 'block' : 'none',
display:
currentLocation.pathname === routes.installations + 'tree' ||
currentLocation.pathname === routes.installations + routes.tree ||
currentLocation.pathname.includes('folder')
? 'block'
: 'none',
'.MuiTreeItem-content': {
width: 'inherit',

View File

@ -1,79 +1,47 @@
import React, { ChangeEvent, useContext, useEffect, useState } from 'react';
import {
Alert,
Box,
Card,
CardContent,
CircularProgress,
Container,
Grid,
IconButton,
Modal,
Tab,
Tabs,
TextField,
Typography,
useTheme
} from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material';
import { Card, Grid, Tab, Tabs } from '@mui/material';
import { I_Folder } from 'src/interfaces/InstallationTypes';
import Button from '@mui/material/Button';
import FolderForm from './folderForm';
import InstallationForm from '../Installations/installationForm';
import { TokenContext } from 'src/contexts/tokenContext';
import { UserContext } from 'src/contexts/userContext';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import AccessContextProvider from 'src/contexts/AccessContextProvider';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import Access from '../ManageAccess/Access';
import { FormattedMessage } from 'react-intl';
import { Link, Route, Routes, useLocation } from 'react-router-dom';
import routes from '../../../Resources/routes.json';
import TreeInformation from './Information';
interface singleFolderProps {
current_folder: I_Folder;
}
function Folder(props: singleFolderProps) {
const theme = useTheme();
const [currentTab, setCurrentTab] = useState<string>('folder');
const [currentTab, setCurrentTab] = useState<string>(undefined);
const [formValues, setFormValues] = useState(props.current_folder);
const [openModalFolder, setOpenModalFolder] = useState(false);
const [openModalInstallation, setOpenModalInstallation] = useState(false);
const requiredFields = ['name'];
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const location = useLocation();
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const [isRowHovered, setHoveredRow] = useState(-1);
const [selectedUser, setSelectedUser] = useState<number>(-1);
const selectedBulkActions = selectedUser !== -1;
const searchParams = new URLSearchParams(location.search);
const folderId = parseInt(searchParams.get('folder'));
const [openModalDeleteFolder, setOpenModalDeleteFolder] = useState(false);
const { currentUser } = context;
const installationContext = useContext(InstallationsContext);
const {
loading,
setLoading,
error,
setError,
updated,
setUpdated,
updateFolder,
deleteFolder
} = installationContext;
const { setError } = installationContext;
useEffect(() => {
setFormValues(props.current_folder);
}, [props.current_folder]);
useEffect(() => {
let path = location.pathname.split('/');
setCurrentTab(path[path.length - 1]);
}, [location]);
if (formValues == undefined) {
return null;
}
const tabs = [
{
value: 'folder',
label: <FormattedMessage id="folder" defaultMessage="Folder" />
value: 'information',
label: <FormattedMessage id="information" defaultMessage="Information" />
},
{
value: 'manage',
@ -88,175 +56,8 @@ function Folder(props: singleFolderProps) {
setError(false);
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSelectOneUser = (installationID: number): void => {
if (selectedUser != installationID) {
setSelectedUser(installationID);
} else {
setSelectedUser(-1);
}
};
const handleRowMouseEnter = (id: number) => {
setHoveredRow(id);
};
const handleRowMouseLeave = () => {
setHoveredRow(-1);
};
const handleFolderInformationUpdate = (e) => {
setLoading(true);
setError(false);
updateFolder(formValues);
};
const handleNewInstallationInsertion = (e) => {
setOpenModalInstallation(true);
};
const handleNewFolderInsertion = (e) => {
setOpenModalFolder(true);
};
const handleDeleteFolder = (e) => {
setLoading(true);
setError(false);
setOpenModalDeleteFolder(true);
};
const deleteFolderModalHandle = (e) => {
setOpenModalDeleteFolder(false);
deleteFolder(formValues);
setLoading(false);
};
const deleteFolderModalHandleCancel = (e) => {
setOpenModalDeleteFolder(false);
setLoading(false);
};
const handleFolderFormSubmit = () => {
setOpenModalFolder(false);
setOpenModalInstallation(false);
};
const handleInstallationFormSubmit = () => {
setOpenModalFolder(false);
setOpenModalInstallation(false);
};
const handleFormCancel = () => {
setOpenModalFolder(false);
setOpenModalInstallation(false);
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!formValues[field]) {
return false;
}
}
return true;
};
if (folderId == props.current_folder.id) {
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 && (
<FolderForm
cancel={handleFormCancel}
submit={handleFolderFormSubmit}
parentid={props.current_folder.id}
/>
)}
{openModalInstallation && (
<InstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}
parentid={props.current_folder.id}
/>
)}
<Grid item xs={12} md={12}>
<TabsContainerWrapper>
<Tabs
@ -268,7 +69,18 @@ function Folder(props: singleFolderProps) {
indicatorColor="primary"
>
{tabs.map((tab) => (
<Tab key={tab.value} label={tab.label} value={tab.value} />
<Tab
key={tab.value}
label={tab.label}
value={tab.value}
component={Link}
to={
location.pathname.substring(
0,
location.pathname.lastIndexOf('/') + 1
) + routes[tab.value]
}
/>
))}
</Tabs>
</TabsContainerWrapper>
@ -280,202 +92,34 @@ function Folder(props: singleFolderProps) {
alignItems="stretch"
spacing={0}
>
{currentTab === 'folder' && (
<Container maxWidth="xl">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12} md={12}>
<CardContent>
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '50ch' }
}}
noValidate
autoComplete="off"
>
<div>
<TextField
label={
<FormattedMessage
id="name"
defaultMessage="Name"
/>
<Routes>
<Route
path={routes.information}
element={
<TreeInformation
folder={props.current_folder}
></TreeInformation>
}
name="name"
value={formValues.name}
onChange={handleChange}
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="information"
defaultMessage="Information"
/>
}
name="information"
value={formValues.information}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleDeleteFolder}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="deleteFolder"
defaultMessage="Delete Folder"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleNewFolderInsertion}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="addNewFolder"
defaultMessage="Add new folder"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleNewInstallationInsertion}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="addNewInstallation"
defaultMessage="Add new installation"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleFolderInformationUpdate}
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)} // Set error state to false on click
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>
</Container>
)}
{currentTab === 'manage' && currentUser.hasWriteAccess && (
<Route
path={routes.manage}
element={
<AccessContextProvider>
<Access
currentResource={formValues}
resourceType="folder"
></Access>
</AccessContextProvider>
}
/>
)}
</Routes>
</Grid>
</Card>
</Grid>
</>
);
} else {
return null;
}
}
export default Folder;

View File

@ -0,0 +1,381 @@
import {
Alert,
Box,
CardContent,
CircularProgress,
Container,
Grid,
IconButton,
Modal,
TextField,
Typography,
useTheme
} from '@mui/material';
import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material';
import React, { useContext, useState } from 'react';
import { I_Folder } from '../../../interfaces/InstallationTypes';
import { UserContext } from '../../../contexts/userContext';
import FolderForm from './folderForm';
import InstallationForm from '../Installations/installationForm';
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
interface TreeInformationProps {
folder: I_Folder;
}
function TreeInformation(props: TreeInformationProps) {
if (props.folder === null) {
return null;
}
const theme = useTheme();
const context = useContext(UserContext);
const { currentUser } = context;
const [formValues, setFormValues] = useState(props.folder);
const [openModalFolder, setOpenModalFolder] = useState(false);
const [openModalInstallation, setOpenModalInstallation] = useState(false);
const requiredFields = ['name'];
const [openModalDeleteFolder, setOpenModalDeleteFolder] = useState(false);
const installationContext = useContext(InstallationsContext);
const {
loading,
setLoading,
error,
setError,
updated,
setUpdated,
updateFolder,
deleteFolder
} = installationContext;
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleFolderInformationUpdate = () => {
setLoading(true);
setError(false);
updateFolder(formValues);
};
const handleNewInstallationInsertion = () => {
setOpenModalInstallation(true);
};
const handleNewFolderInsertion = () => {
setOpenModalFolder(true);
};
const handleDeleteFolder = () => {
setLoading(true);
setError(false);
setOpenModalDeleteFolder(true);
};
const deleteFolderModalHandle = () => {
setOpenModalDeleteFolder(false);
deleteFolder(formValues);
setLoading(false);
};
const deleteFolderModalHandleCancel = () => {
setOpenModalDeleteFolder(false);
setLoading(false);
};
const handleFolderFormSubmit = () => {
setOpenModalFolder(false);
setOpenModalInstallation(false);
};
const handleInstallationFormSubmit = () => {
setOpenModalFolder(false);
setOpenModalInstallation(false);
};
const handleFormCancel = () => {
setOpenModalFolder(false);
setOpenModalInstallation(false);
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!formValues[field]) {
return false;
}
}
return true;
};
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 && (
<FolderForm
cancel={handleFormCancel}
submit={handleFolderFormSubmit}
parentid={props.folder.id}
/>
)}
{openModalInstallation && (
<InstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}
parentid={props.folder.id}
/>
)}
<Container maxWidth="xl">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12} md={12}>
<CardContent>
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '50ch' }
}}
noValidate
autoComplete="off"
>
<div>
<TextField
label={<FormattedMessage id="name" defaultMessage="Name" />}
name="name"
value={formValues.name}
onChange={handleChange}
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="information"
defaultMessage="Information"
/>
}
name="information"
value={formValues.information}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleDeleteFolder}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="deleteFolder"
defaultMessage="Delete Folder"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleNewFolderInsertion}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="addNewFolder"
defaultMessage="Add new folder"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleNewInstallationInsertion}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="addNewInstallation"
defaultMessage="Add new installation"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleFolderInformationUpdate}
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)} // Set error state to false on click
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>
</Container>
</>
);
}
export default TreeInformation;

View File

@ -5,8 +5,10 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import CustomTreeItem from './CustomTreeItem';
import Installation from '../Installations/Installation';
import Folder from './Folder';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import { Route, Routes } from 'react-router-dom';
import routes from '../../../Resources/routes.json';
import Folder from './Folder';
function InstallationTree() {
const { foldersAndInstallations, fetchAllFoldersAndInstallations } =
@ -65,24 +67,38 @@ function InstallationTree() {
</TreeView>
</Grid>
<Routes>
{foldersAndInstallations.map((installation) => {
if (installation.type == 'Installation') {
return (
<Route
key={installation.id}
path={routes.installation + installation.id + '*'}
element={
<Installation
key={installation.id + installation.type}
key={installation.id}
current_installation={installation}
type="tree"
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,7 +14,6 @@ import {
} from '@mui/material';
import { InnovEnergyUser } from 'src/interfaces/UserTypes';
import User from './User';
import { useNavigate } from 'react-router-dom';
interface FlatUsersViewProps {
users: InnovEnergyUser[];
@ -23,15 +22,10 @@ interface FlatUsersViewProps {
const FlatUsersView = (props: FlatUsersViewProps) => {
const [selectedUser, setSelectedUser] = useState<number>(-1);
const selectedBulkActions = selectedUser !== -1;
const navigate = useNavigate();
const handleSelectOneUser = (installationID: number): void => {
if (selectedUser != installationID) {
setSelectedUser(installationID);
// navigate(routes.users + '?user=' + installationID.toString(), {
// replace: true
// });
const handleSelectOneUser = (userID: number): void => {
if (selectedUser != userID) {
setSelectedUser(userID);
} else {
setSelectedUser(-1);
}

View File

@ -7,6 +7,13 @@ import { FetchResult } from '../dataCache/dataCache';
import { I_S3Credentials } from './S3Types';
import { UnixTime } from '../dataCache/time';
export interface chartInfoInterface {
magnitude: number;
unit: string;
min: number;
max: number;
}
export interface overviewInterface {
soc: chartInfoInterface;
temperature: chartInfoInterface;
@ -18,13 +25,6 @@ export interface overviewInterface {
overview: chartInfoInterface;
}
export interface chartInfoInterface {
magnitude: number;
unit: string;
min: number;
max: number;
}
export interface chartAggregatedDataInterface {
minsoc: { name: string; data: number[] };
maxsoc: { name: string; data: number[] };
@ -45,6 +45,197 @@ export interface chartDataInterface {
dcBusVoltage: { name: string; data: number[] };
}
export interface BatteryDataInterface {
Soc: { name: string; data: [] };
Temperature: { name: string; data: [] };
Power: { name: string; data: [] };
Voltage: { name: string; data: [] };
Current: { name: string; data: [] };
}
export interface BatteryOverviewInterface {
Soc: chartInfoInterface;
Temperature: chartInfoInterface;
Power: chartInfoInterface;
Voltage: chartInfoInterface;
Current: chartInfoInterface;
}
export const transformInputToBatteryViewData = async (
s3Credentials: I_S3Credentials,
startTimestamp: UnixTime,
endTimestamp: UnixTime
): Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> => {
const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999;
const categories = ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
const pathCategories = [
'Soc',
'Temperatures/Cells/Center',
'Dc/Power',
'Dc/Voltage',
'Dc/Current'
];
const pathsToSearch = [
'/Battery/Devices/1/',
'/Battery/Devices/2/',
'/Battery/Devices/3/',
'/Battery/Devices/4/',
'/Battery/Devices/5/',
'/Battery/Devices/6/',
'/Battery/Devices/7/',
'/Battery/Devices/8/',
'/Battery/Devices/9/',
'/Battery/Devices/10/'
];
const pathsToSave = [
'Battery1',
'Battery2',
'Battery3',
'Battery4',
'Battery5',
'Battery6',
'Battery7',
'Battery8',
'Battery9',
'Battery10'
];
const chartData: BatteryDataInterface = {
Soc: { name: 'State Of Charge', data: [] },
Temperature: { name: 'Temperature', data: [] },
Power: { name: 'Power', data: [] },
Voltage: { name: 'Voltage', data: [] },
Current: { name: 'Voltage', data: [] }
};
const chartOverview: BatteryOverviewInterface = {
Soc: { magnitude: 0, unit: '', min: 0, max: 0 },
Temperature: { magnitude: 0, unit: '', min: 0, max: 0 },
Power: { magnitude: 0, unit: '', min: 0, max: 0 },
Voltage: { magnitude: 0, unit: '', min: 0, max: 0 },
Current: { magnitude: 0, unit: '', min: 0, max: 0 }
};
categories.forEach((category) => {
chartData[category].data = [];
pathsToSave.forEach((path) => {
chartData[category].data[path] = { name: path, data: [] };
});
chartOverview[category] = {
magnitude: 0,
unit: '',
min: MAX_NUMBER,
max: -MAX_NUMBER
};
});
let adjustedTimestampArray = [];
let startTimestampToNum = Number(startTimestamp);
if (startTimestampToNum % 2 != 0) {
startTimestampToNum += 1;
}
let startUnixTime = UnixTime.fromTicks(startTimestampToNum);
let diff = endTimestamp.ticks - startUnixTime.ticks;
const timestampPromises = [];
while (startUnixTime < endTimestamp) {
timestampPromises.push(fetchData(startUnixTime, s3Credentials));
startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + diff / 100);
if (startUnixTime.ticks % 2 !== 0) {
startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + 1);
}
const adjustedTimestamp = new Date(startUnixTime.ticks * 1000);
adjustedTimestamp.setHours(adjustedTimestamp.getHours() + 1);
adjustedTimestampArray.push(adjustedTimestamp);
}
const results = await Promise.all(timestampPromises);
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (
result === FetchResult.notAvailable ||
result === FetchResult.tryLater
) {
// Handle not available or try later case
} else {
for (
let category_index = 0;
category_index < pathCategories.length;
category_index++
) {
let category = categories[category_index];
for (let j = 0; j < pathsToSearch.length; j++) {
let path = pathsToSearch[j] + pathCategories[category_index];
if (result[path]) {
const value = result[path];
if (value.value < chartOverview[category].min) {
chartOverview[category].min = value.value;
}
if (value.value > chartOverview[category].max) {
chartOverview[category].max = value.value;
}
chartData[category].data[pathsToSave[j]].data.push([
adjustedTimestampArray[i],
value.value
]);
} else {
//data[path].push([adjustedTimestamp, null]);
}
}
}
}
}
categories.forEach((category) => {
let value = Math.max(
Math.abs(chartOverview[category].max),
Math.abs(chartOverview[category].min)
);
let magnitude = 0;
if (value < 0) {
value = -value;
}
while (value >= 1000) {
value /= 1000;
magnitude++;
}
chartOverview[category].magnitude = magnitude;
});
chartOverview.Soc.unit = '(%)';
chartOverview.Soc.min = 0;
chartOverview.Soc.max = 100;
chartOverview.Temperature.unit = '(°C)';
chartOverview.Power.unit =
'(' + prefixes[chartOverview['Power'].magnitude] + 'W' + ')';
chartOverview.Voltage.unit =
'(' + prefixes[chartOverview['Voltage'].magnitude] + 'V' + ')';
chartOverview.Current.unit =
'(' + prefixes[chartOverview['Current'].magnitude] + 'A' + ')';
return {
chartData: chartData,
chartOverview: chartOverview
};
};
export const transformInputToDailyData = async (
s3Credentials: I_S3Credentials,
startTimestamp: UnixTime,
@ -53,8 +244,6 @@ export const transformInputToDailyData = async (
chartData: chartDataInterface;
chartOverview: overviewInterface;
}> => {
const data = {};
const overviewData = {};
const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999;
const pathsToSearch = [
@ -65,6 +254,14 @@ export const transformInputToDailyData = async (
'/PvOnDc/Dc/Power',
'/DcDc/Dc/Link/Voltage'
];
const categories = [
'soc',
'temperature',
'dcPower',
'gridPower',
'pvProduction',
'dcBusVoltage'
];
const chartData: chartDataInterface = {
soc: { name: 'State Of Charge', data: [] },
@ -86,9 +283,9 @@ export const transformInputToDailyData = async (
overview: { magnitude: 0, unit: '', min: 0, max: 0 }
};
pathsToSearch.forEach((path) => {
data[path] = [];
overviewData[path] = {
categories.forEach((category) => {
chartData[category].data = [];
chartOverview[category] = {
magnitude: 0,
unit: '',
min: MAX_NUMBER,
@ -130,29 +327,33 @@ export const transformInputToDailyData = async (
// Handle not available or try later case
} else {
// eslint-disable-next-line @typescript-eslint/no-loop-func
let category_index = 0;
pathsToSearch.forEach((path) => {
if (result[path]) {
const value = result[path];
if (value.value < overviewData[path].min) {
overviewData[path].min = value.value;
if (value.value < chartOverview[categories[category_index]].min) {
chartOverview[categories[category_index]].min = value.value;
}
if (value.value > overviewData[path].max) {
overviewData[path].max = value.value;
if (value.value > chartOverview[categories[category_index]].max) {
chartOverview[categories[category_index]].max = value.value;
}
data[path].push([adjustedTimestampArray[i], value.value]);
chartData[categories[category_index]].data.push([
adjustedTimestampArray[i],
value.value
]);
} else {
//data[path].push([adjustedTimestamp, null]);
}
category_index++;
});
}
}
pathsToSearch.forEach((path) => {
categories.forEach((category) => {
let value = Math.max(
Math.abs(overviewData[path].max),
Math.abs(overviewData[path].min)
Math.abs(chartOverview[category].max),
Math.abs(chartOverview[category].min)
);
let magnitude = 0;
@ -163,81 +364,35 @@ export const transformInputToDailyData = async (
value /= 1000;
magnitude++;
}
overviewData[path].magnitude = magnitude;
chartOverview[category].magnitude = magnitude;
});
let path = '/Battery/Soc';
chartData.soc.data = data[path];
chartOverview.soc = {
unit: '(%)',
magnitude: overviewData[path].magnitude,
min: 0,
max: 100
};
path = '/Battery/Temperature';
chartData.temperature.data = data[path];
chartOverview.temperature = {
unit: '(°C)',
magnitude: overviewData[path].magnitude,
min: overviewData[path].min,
max: overviewData[path].max
};
path = '/Battery/Dc/Power';
chartData.dcPower.data = data[path];
chartOverview.dcPower = {
magnitude: overviewData[path].magnitude,
unit: '(' + prefixes[overviewData[path].magnitude] + 'W' + ')',
min: overviewData[path].min,
max: overviewData[path].max
};
path = '/GridMeter/Ac/Power/Active';
chartData.gridPower.data = data[path];
chartOverview.gridPower = {
magnitude: overviewData[path].magnitude,
unit: '(' + prefixes[overviewData[path].magnitude] + 'W' + ')',
min: overviewData[path].min,
max: overviewData[path].max
};
path = '/PvOnDc/Dc/Power';
chartData.pvProduction.data = data[path];
chartOverview.pvProduction = {
magnitude: overviewData[path].magnitude,
unit: '(' + prefixes[overviewData[path].magnitude] + 'W' + ')',
min: overviewData[path].min,
max: overviewData[path].max
};
path = '/DcDc/Dc/Link/Voltage';
chartData.dcBusVoltage.data = data[path];
chartOverview.dcBusVoltage = {
magnitude: overviewData[path].magnitude,
unit: '(' + prefixes[overviewData[path].magnitude] + 'V' + ')',
min: overviewData[path].min,
max: overviewData[path].max
};
chartOverview.soc.unit = '(%)';
chartOverview.soc.min = 0;
chartOverview.soc.max = 100;
chartOverview.temperature.unit = '(°C)';
chartOverview.dcPower.unit =
'(' + prefixes[chartOverview['dcPower'].magnitude] + 'W' + ')';
chartOverview.gridPower.unit =
'(' + prefixes[chartOverview['gridPower'].magnitude] + 'W' + ')';
chartOverview.pvProduction.unit =
'(' + prefixes[chartOverview['pvProduction'].magnitude] + 'W' + ')';
chartOverview.dcBusVoltage.unit =
'(' + prefixes[chartOverview['dcBusVoltage'].magnitude] + 'V' + ')';
chartOverview.overview = {
magnitude: Math.max(
overviewData['/GridMeter/Ac/Power/Active'].magnitude,
overviewData['/PvOnDc/Dc/Power'].magnitude
chartOverview['gridPower'].magnitude,
chartOverview['pvProduction'].magnitude
),
unit: '(kW)',
min: Math.min(
overviewData['/GridMeter/Ac/Power/Active'].min,
overviewData['/PvOnDc/Dc/Power'].min
chartOverview['gridPower'].min,
chartOverview['pvProduction'].min
),
max: Math.max(
overviewData['/GridMeter/Ac/Power/Active'].max,
overviewData['/PvOnDc/Dc/Power'].max
chartOverview['gridPower'].max,
chartOverview['pvProduction'].max
)
};
@ -277,6 +432,17 @@ export const transformInputToAggregatedData = async (
'/HeatingPower'
];
const categories = [
'minsoc',
'maxsoc',
'pvProduction',
'dcChargingPower',
'heatingPower',
'dcDischargingPower',
'gridImportPower',
'gridExportPower'
];
const chartAggregatedData: chartAggregatedDataInterface = {
minsoc: { name: 'min SOC', data: [] },
maxsoc: { name: 'max SOC', data: [] },

View File

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