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; subscribedNow = true;
_subscribeToQueueForTheFirstTime = true; _subscribeToQueueForTheFirstTime = true;
_prevSalimaxState = currentSalimaxState.Status;
_subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp); _subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp);
} }

View File

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

View File

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

View File

@ -18,5 +18,7 @@
"information": "information", "information": "information",
"configuration": "configuration", "configuration": "configuration",
"login": "/login/", "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 { import {
Container,
Grid,
Paper, Paper,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableContainer, TableContainer,
TableHead, TableHead,
TableRow, TableRow
useTheme
} from '@mui/material'; } from '@mui/material';
import { TopologyValues } from '../Log/graph.util'; 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 { interface BatteryViewProps {
values: TopologyValues; values: TopologyValues;
s3Credentials: I_S3Credentials;
} }
function BatteryView(props: BatteryViewProps) { function BatteryView(props: BatteryViewProps) {
if (props.values === null) { if (props.values === null) {
return null; return null;
} }
const theme = useTheme(); const currentPath = useLocation();
const searchParams = new URLSearchParams(location.search); const navigate = useNavigate();
const installationId = parseInt(searchParams.get('installation')); const [currentTab, setCurrentTab] = useState<string>(undefined);
const currentTab = searchParams.get('tab');
const numOfBatteries = props.values.batteryView.values[0].value const numOfBatteries = props.values.batteryView.values[0].value
.toString() .toString()
.split(',').length; .split(',').length;
const batteryData = []; const batteryData = [];
let batteryId = 1; let batteryId = 1;
// Use a for loop to generate battery data // Use a for loop to generate battery data
for (let index = 1; index <= numOfBatteries * 7; index += 7) { for (let index = 1; index <= numOfBatteries * 7; index += 7) {
const battery = { const battery = {
@ -61,155 +71,225 @@ function BatteryView(props: BatteryViewProps) {
batteryData.push(battery); batteryData.push(battery);
} }
const handleMainStatsButton = () => {
navigate(routes.mainstats);
};
useEffect(() => {
let path = currentPath.pathname.split('/');
setCurrentTab(path[path.length - 1]);
}, [currentPath]);
return ( return (
<TableContainer component={Paper}> <>
<Table sx={{ minWidth: 650 }} aria-label="simple table"> <Container maxWidth="xl">
<TableHead> <Grid container>
<TableRow> <Routes>
<TableCell align="center">Battery</TableCell> <Route
<TableCell align="center">Firmware</TableCell> path={routes.mainstats}
<TableCell align="center">Power</TableCell> element={
<TableCell align="center">Voltage</TableCell> <MainStats s3Credentials={props.s3Credentials}></MainStats>
<TableCell align="center">SoC</TableCell> }
<TableCell align="center">Temperature</TableCell> />
<TableCell align="center">Warnings</TableCell> </Routes>
<TableCell align="center">Alarms</TableCell> {currentTab === 'batteryview' && (
</TableRow> <Grid item xs={6} md={6}>
</TableHead> <Button
<TableBody> variant="contained"
{batteryData.map((battery) => (
<TableRow
key={battery.BatteryId}
style={{
height: '10px'
}}
>
<TableCell
component="th"
scope="row"
align="center"
sx={{ fontWeight: 'bold' }}
>
{battery.BatteryId}
</TableCell>
<TableCell
sx={{ sx={{
width: '10%', marginTop: '20px',
textAlign: 'center' backgroundColor: '#808080',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}} }}
> >
{battery.FwVersion} <FormattedMessage
</TableCell> id="main_stats"
<TableCell defaultMessage="Battery View"
sx={{ />
width: '10%', </Button>
textAlign: 'center'
}}
>
{battery.Power}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor: <Button
battery.Voltage < 44 || battery.Voltage > 57 variant="contained"
? '#FF033E' onClick={handleMainStatsButton}
: '#32CD32',
color: battery.Voltage === '' ? 'white' : 'inherit'
}}
>
{battery.Voltage}
</TableCell>
<TableCell
sx={{ sx={{
width: '10%', marginTop: '20px',
textAlign: 'center', marginLeft: '20px',
backgroundColor: // backgroundColor: mainStatsData ? '#808080' : '#ffc04d',
battery.Soc < 20 backgroundColor: '#ffc04d',
? '#FF033E' color: '#000000',
: battery.Soc < 50 '&:hover': { bgcolor: '#f7b34d' }
? '#ffbf00 '
: '#32CD32',
color: battery.Soc === '' ? 'white' : 'inherit'
}} }}
> >
{battery.Soc} <FormattedMessage id="main_stats" defaultMessage="Main Stats" />
</TableCell> </Button>
<TableCell </Grid>
sx={{ )}
width: '10%', </Grid>
textAlign: 'center',
backgroundColor:
battery.AverageTemperature > 270 ? '#FF033E' : '#32CD32 '
}}
>
{battery.AverageTemperature}
</TableCell>
<TableCell {currentTab === 'batteryview' && (
style={{ <TableContainer
width: '20%', component={Paper}
textAlign: 'center', sx={{ marginTop: '20px', marginBottom: '20px' }}
padding: '8px', >
fontWeight: battery.Warnings !== '' ? 'bold' : 'inherit', <Table sx={{ minWidth: 650 }} aria-label="simple table">
backgroundColor: <TableHead>
battery.Warnings === '' ? 'inherit' : '#ff9900', <TableRow>
color: battery.Warnings != '' ? 'black' : 'inherit' <TableCell align="center">Battery</TableCell>
}} <TableCell align="center">Firmware</TableCell>
> <TableCell align="center">Power</TableCell>
{battery.Warnings === '' ? ( <TableCell align="center">Voltage</TableCell>
'None' <TableCell align="center">SoC</TableCell>
) : battery.Warnings.split(';').length > 1 ? ( <TableCell align="center">Temperature</TableCell>
<Link <TableCell align="center">Warnings</TableCell>
style={{ color: 'black' }} <TableCell align="center">Alarms</TableCell>
to={ </TableRow>
'?installation=' + </TableHead>
installationId + <TableBody>
'&tab=log' + {batteryData.map((battery) => (
'&open=warning' <TableRow
} key={battery.BatteryId}
style={{
height: '10px'
}}
> >
Multiple Warnings <TableCell
</Link> component="th"
) : ( scope="row"
battery.Warnings align="center"
)} sx={{ fontWeight: 'bold' }}
</TableCell> >
<TableCell {battery.BatteryId}
sx={{ </TableCell>
width: '20%', <TableCell
textAlign: 'center', sx={{
fontWeight: battery.Alarms !== '' ? 'bold' : 'inherit', width: '10%',
backgroundColor: textAlign: 'center'
battery.Alarms === '' ? 'inherit' : '#FF033E', }}
color: battery.Alarms != '' ? 'black' : 'inherit' >
}} {battery.FwVersion}
> </TableCell>
{battery.Alarms === '' ? ( <TableCell
'None' sx={{
) : battery.Alarms.split(';').length > 1 ? ( width: '10%',
<Link textAlign: 'center'
style={{ color: 'black' }} }}
to={ >
'?installation=' + {battery.Power}
installationId + </TableCell>
'&tab=log' + <TableCell
'&open=error' sx={{
} width: '10%',
> textAlign: 'center',
Multiple Alarms
</Link> backgroundColor:
) : ( battery.Voltage < 44 || battery.Voltage > 57
battery.Alarms ? '#FF033E'
)} : '#32CD32',
</TableCell> color: battery.Voltage === '' ? 'white' : 'inherit'
</TableRow> }}
))} >
</TableBody> {battery.Voltage}
</Table> </TableCell>
</TableContainer> <TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
battery.Soc < 20
? '#FF033E'
: battery.Soc < 50
? '#ffbf00 '
: '#32CD32',
color: battery.Soc === '' ? 'white' : 'inherit'
}}
>
{battery.Soc}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
battery.AverageTemperature > 270
? '#FF033E'
: '#32CD32 '
}}
>
{battery.AverageTemperature}
</TableCell>
<TableCell
style={{
width: '20%',
textAlign: 'center',
padding: '8px',
fontWeight:
battery.Warnings !== '' ? 'bold' : 'inherit',
backgroundColor:
battery.Warnings === '' ? 'inherit' : '#ff9900',
color: battery.Warnings != '' ? 'black' : 'inherit'
}}
>
{battery.Warnings === '' ? (
'None'
) : battery.Warnings.split(';').length > 1 ? (
<Link
style={{ color: 'black' }}
to={
currentPath.pathname.substring(
0,
currentPath.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=warning'
}
>
Multiple Warnings
</Link>
) : (
battery.Warnings
)}
</TableCell>
<TableCell
sx={{
width: '20%',
textAlign: 'center',
fontWeight: battery.Alarms !== '' ? 'bold' : 'inherit',
backgroundColor:
battery.Alarms === '' ? 'inherit' : '#FF033E',
color: battery.Alarms != '' ? 'black' : 'inherit'
}}
>
{battery.Alarms === '' ? (
'None'
) : battery.Alarms.split(';').length > 1 ? (
<Link
style={{ color: 'black' }}
to={
currentPath.pathname.substring(
0,
currentPath.pathname.lastIndexOf('/') + 1
) +
routes.log +
'?open=error'
}
>
Multiple Alarms
</Link>
) : (
battery.Alarms
)}
</TableCell>
</TableRow>
))}
</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 { import {
Card, Card,
CircularProgress, CircularProgress,
@ -13,11 +13,11 @@ import {
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import { I_Installation } from 'src/interfaces/InstallationTypes'; import { I_Installation } from 'src/interfaces/InstallationTypes';
import Installation from './Installation';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider'; import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
import { FormattedMessage } from 'react-intl'; 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 { interface FlatInstallationViewProps {
installations: I_Installation[]; installations: I_Installation[];
@ -28,31 +28,32 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
const webSocketContext = useContext(WebSocketContext); const webSocketContext = useContext(WebSocketContext);
const { getStatus } = webSocketContext; const { getStatus } = webSocketContext;
const navigate = useNavigate(); const navigate = useNavigate();
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1); const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
const currentLocation = useLocation();
const handleSelectOneInstallation = (installationID: number): void => { const handleSelectOneInstallation = (installationID: number): void => {
if (selectedInstallation != installationID) { if (selectedInstallation != installationID) {
setSelectedInstallation(installationID); setSelectedInstallation(installationID);
navigate(`?installation=${installationID}`, { setSelectedInstallation(-1);
replace: true
}); navigate(
routes.installations +
routes.list +
routes.installation +
`${installationID}` +
'/' +
routes.live,
{
replace: true
}
);
} else { } else {
setSelectedInstallation(-1); setSelectedInstallation(-1);
} }
}; };
useEffect(() => {
setSelectedInstallation(installationId);
}, [installationId]);
const theme = useTheme(); const theme = useTheme();
const findInstallation = (id: number) => {
return props.installations.find((installation) => installation.id === id);
};
const handleRowMouseEnter = (id: number) => { const handleRowMouseEnter = (id: number) => {
setHoveredRow(id); setHoveredRow(id);
}; };
@ -63,7 +64,16 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
return ( return (
<Grid container spacing={1} sx={{ marginTop: 0.1 }}> <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> <Card>
<TableContainer> <TableContainer>
<Table> <Table>
@ -225,14 +235,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
</TableContainer> </TableContainer>
</Card> </Card>
</Grid> </Grid>
{props.installations.map((installation) => (
<Installation
key={installation.id}
current_installation={findInstallation(installation.id)}
type="installation"
></Installation>
))}
</Grid> </Grid>
); );
}; };

View File

@ -1,24 +1,7 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { import { Card, CircularProgress, Grid, Typography } from '@mui/material';
Alert,
Box,
Card,
CardContent,
CircularProgress,
Container,
Grid,
IconButton,
Modal,
TextField,
Typography,
useTheme
} from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material';
import { I_Installation } from 'src/interfaces/InstallationTypes'; 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 { UserContext } from 'src/contexts/userContext';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import AccessContextProvider from 'src/contexts/AccessContextProvider'; import AccessContextProvider from 'src/contexts/AccessContextProvider';
import Access from '../ManageAccess/Access'; import Access from '../ManageAccess/Access';
import Log from 'src/content/dashboards/Log/Log'; 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 { fetchData } from 'src/content/dashboards/Installations/fetchData';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import BatteryView from '../BatteryView/BatteryView'; 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 { interface singleInstallationProps {
current_installation?: I_Installation; current_installation?: I_Installation;
@ -43,80 +29,20 @@ interface singleInstallationProps {
} }
function Installation(props: 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 context = useContext(UserContext);
const { currentUser, setUser } = context; const { currentUser } = context;
const tokencontext = useContext(TokenContext); const location = useLocation();
const { removeToken } = tokencontext;
const installationContext = useContext(InstallationsContext);
const {
updateInstallation,
loading,
setLoading,
error,
setError,
updated,
setUpdated,
deleteInstallation
} = installationContext;
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false); const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
const webSocketsContext = useContext(WebSocketContext); const webSocketsContext = useContext(WebSocketContext);
const { getStatus } = webSocketsContext; const { getStatus } = webSocketsContext;
const searchParams = new URLSearchParams(location.search); const [currentTab, setCurrentTab] = useState<string>(undefined);
const installationId = parseInt(searchParams.get('installation'));
const currentTab = searchParams.get('tab');
const [values, setValues] = useState<TopologyValues | null>(null); const [values, setValues] = useState<TopologyValues | null>(null);
const [openModalDeleteInstallation, setOpenModalDeleteInstallation] =
useState(false);
const status = getStatus(props.current_installation.id); const status = getStatus(props.current_installation.id);
if (formValues == undefined) { if (props.current_installation == undefined) {
return null; 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 = { const S3data = {
s3Region: props.current_installation.s3Region, s3Region: props.current_installation.s3Region,
s3Provider: props.current_installation.s3Provider, s3Provider: props.current_installation.s3Provider,
@ -132,15 +58,10 @@ function Installation(props: singleInstallationProps) {
const fetchDataPeriodically = async () => { const fetchDataPeriodically = async () => {
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20)); const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
const date = now.toDate();
try { try {
const res = await fetchData(now, s3Credentials); const res = await fetchData(now, s3Credentials);
// if (!isMounted) {
// return false;
// }
if (res != FetchResult.notAvailable && res != FetchResult.tryLater) { if (res != FetchResult.notAvailable && res != FetchResult.tryLater) {
setValues( setValues(
extractValues({ extractValues({
@ -166,18 +87,22 @@ function Installation(props: singleInstallationProps) {
} }
}; };
useEffect(() => {
let path = location.pathname.split('/');
setCurrentTab(path[path.length - 1]);
}, [location]);
useEffect(() => { useEffect(() => {
if ( if (
installationId == props.current_installation.id && currentTab == 'live' ||
(currentTab == 'live' || currentTab == 'configuration' ||
currentTab == 'configuration' || currentTab == 'batteryview'
currentTab == 'batteryview')
) { ) {
//let isMounted = true;
setFormValues(props.current_installation);
var interval; var interval;
if (currentTab == 'live' || currentTab == 'batteryview') { if (currentTab == 'live' || currentTab == 'batteryview') {
fetchDataPeriodically();
interval = setInterval(fetchDataPeriodically, 2000); interval = setInterval(fetchDataPeriodically, 2000);
} }
if (currentTab == 'configuration') { if (currentTab == 'configuration') {
@ -186,145 +111,47 @@ function Installation(props: singleInstallationProps) {
// Cleanup function to cancel interval and update isMounted when unmounted // Cleanup function to cancel interval and update isMounted when unmounted
return () => { return () => {
//isMounted = false;
if (currentTab == 'live' || currentTab == 'batteryview') { if (currentTab == 'live' || currentTab == 'batteryview') {
clearInterval(interval); clearInterval(interval);
} }
}; };
} }
}, [installationId, currentTab]); }, [currentTab, location.pathname]);
if (installationId == props.current_installation.id) { return (
return ( <>
<> <Grid item xs={12} md={12}>
{openModalDeleteInstallation && ( <div style={{ display: 'flex', alignItems: 'center' }}>
<Modal <Typography
open={openModalDeleteInstallation} fontWeight="bold"
onClose={deleteInstallationModalHandleCancel} color="text.primary"
aria-labelledby="error-modal" noWrap
aria-describedby="error-modal-description" sx={{
marginTop: '-20px',
marginBottom: '10px',
fontSize: '14px'
}}
> >
<Box <FormattedMessage
sx={{ id="installation_name_simple"
position: 'absolute', defaultMessage="Installation Name:"
top: '50%', />
left: '50%', </Typography>
transform: 'translate(-50%, -50%)', <Typography
width: 350, fontWeight="bold"
bgcolor: 'background.paper', color="orange"
borderRadius: 4, noWrap
boxShadow: 24, sx={{
p: 4, marginTop: '-20px',
display: 'flex', marginBottom: '10px',
flexDirection: 'column', marginLeft: '5px',
alignItems: 'center' fontSize: '14px'
}} }}
> >
<Typography {props.current_installation.name}
variant="body1" </Typography>
gutterBottom </div>
sx={{ fontWeight: 'bold' }} {currentTab == 'live' && values && (
>
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
fontWeight="bold"
color="text.primary"
noWrap
sx={{
marginTop: '-20px',
marginBottom: '10px',
fontSize: '14px'
}}
>
<FormattedMessage
id="installation_name_simple"
defaultMessage="Installation Name:"
/>
</Typography>
<Typography
fontWeight="bold"
color="orange"
noWrap
sx={{
marginTop: '-20px',
marginBottom: '10px',
marginLeft: '5px',
fontSize: '14px'
}}
>
{props.current_installation.name}
</Typography>
</div>
{currentTab == 'live' && values && (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Typography
fontWeight="bold"
color="text.primary"
noWrap
sx={{
marginTop: '0px',
marginBottom: '10px',
fontSize: '14px'
}}
>
<FormattedMessage id="mode" defaultMessage="Mode:" />
</Typography>
<Typography
fontWeight="bold"
color="orange"
noWrap
sx={{
marginTop: '0px',
marginBottom: '10px',
marginLeft: '85px',
fontSize: '14px'
}}
>
{values.mode.values[0].value}
</Typography>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
<Typography <Typography
fontWeight="bold" fontWeight="bold"
@ -336,365 +163,172 @@ function Installation(props: singleInstallationProps) {
fontSize: '14px' fontSize: '14px'
}} }}
> >
Status: <FormattedMessage id="mode" defaultMessage="Mode:" />
</Typography> </Typography>
<div <Typography
style={{ fontWeight="bold"
display: 'flex', color="orange"
alignItems: 'center', noWrap
marginLeft: '75px', sx={{
marginTop: '-10px' marginTop: '0px',
marginBottom: '10px',
marginLeft: '85px',
fontSize: '14px'
}} }}
> >
{status === -1 ? ( {values.mode.values[0].value}
<CancelIcon </Typography>
style={{ </div>
width: '23px', )}
height: '23px', <div style={{ display: 'flex', alignItems: 'center' }}>
color: 'red', <Typography
borderRadius: '50%' fontWeight="bold"
}} color="text.primary"
/> noWrap
) : ( sx={{
'' marginTop: '0px',
)} marginBottom: '10px',
fontSize: '14px'
{status === -2 ? ( }}
<CircularProgress >
size={20} Status:
sx={{ </Typography>
color: '#f7b34d' <div
}} style={{
/> display: 'flex',
) : ( alignItems: 'center',
'' marginLeft: '75px',
)} marginTop: '-10px'
}}
<div >
{status === -1 ? (
<CancelIcon
style={{ style={{
width: '20px', width: '23px',
height: '20px', height: '23px',
marginLeft: '2px', color: 'red',
borderRadius: '50%', borderRadius: '50%'
backgroundColor:
status === 2
? 'red'
: status === 1
? 'orange'
: status === -1 || status === -2
? 'transparent'
: 'green'
}} }}
/> />
</div> ) : (
''
)}
{status === -2 ? (
<CircularProgress
size={20}
sx={{
color: '#f7b34d'
}}
/>
) : (
''
)}
<div
style={{
width: '20px',
height: '20px',
marginLeft: '2px',
borderRadius: '50%',
backgroundColor:
status === 2
? 'red'
: status === 1
? 'orange'
: status === -1 || status === -2
? 'transparent'
: 'green'
}}
/>
</div> </div>
</div>
<Card variant="outlined"> <Card variant="outlined">
<Grid <Grid
container container
direction="row" direction="row"
justifyContent="center" justifyContent="center"
alignItems="stretch" alignItems="stretch"
spacing={0} spacing={0}
> >
{currentTab === 'information' && ( <Routes>
<Container maxWidth="xl"> <Route
<Grid path={routes.information}
container element={
direction="row" <Information
justifyContent="center" values={props.current_installation}
alignItems="stretch" s3Credentials={s3Credentials}
spacing={3} type={props.type}
> ></Information>
<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.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 && ( <Route
<> path={routes.batteryview + '*'}
<div> element={
<TextField <BatteryView
label="Vpn IP" values={values}
name="VpnIp" s3Credentials={s3Credentials}
value={formValues.vpnIp} ></BatteryView>
variant="outlined" }
fullWidth />
/>
</div>
<div> <Route
<TextField path={routes.overview}
label="S3 Write Key" element={<Overview s3Credentials={s3Credentials}></Overview>}
name="s3writekey" />
value={formValues.s3WriteKey}
variant="outlined"
fullWidth
/>
</div>
<div> <Route
<TextField path={routes.live}
label="S3 Write Secret Key" element={<Topology values={values}></Topology>}
name="s3writesecretkey" />
value={formValues.s3WriteSecret}
variant="outlined"
fullWidth
/>
</div>
<div> <Route
<TextField path={routes.log}
label="S3 Bucket Name" element={
name="s3writesecretkey" <Log
value={ errorLoadingS3Data={errorLoadingS3Data}
formValues.id + id={props.current_installation.id}
'-3e5b3069-214a-43ee-8d85-57d72000c19d' ></Log>
} }
variant="outlined" />
fullWidth
/>
</div>
</>
)}
<div {currentUser.hasWriteAccess && (
style={{ <Route
display: 'flex', path={routes.configuration}
alignItems: 'center', element={
marginTop: 10 <Configuration
}} values={values}
> id={props.current_installation.id}
{currentUser.hasWriteAccess && ( ></Configuration>
<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' && ( {currentUser.hasWriteAccess && (
<Overview s3Credentials={s3Credentials}></Overview> <Route
path={routes.manage}
element={
<AccessContextProvider>
<Access
currentResource={props.current_installation}
resourceType={props.type}
></Access>
</AccessContextProvider>
}
/>
)} )}
{currentTab === 'batteryview' && (
<BatteryView values={values}></BatteryView> <Route
)} path={'*'}
{currentTab === 'configuration' && currentUser.hasWriteAccess && ( element={<Navigate to={routes.live}></Navigate>}
<Configuration />
values={values} </Routes>
id={installationId} </Grid>
></Configuration> </Card>
)} </Grid>
{currentTab === 'manage' && currentUser.hasWriteAccess && ( </>
<AccessContextProvider> );
<Access
currentResource={formValues}
resourceType={props.type}
></Access>
</AccessContextProvider>
)}
{currentTab === 'live' && <Topology values={values}></Topology>}
{currentTab === 'log' && (
<Log
errorLoadingS3Data={errorLoadingS3Data}
id={props.current_installation.id}
></Log>
)}
</Grid>
</Card>
</Grid>
</>
);
} else {
return null;
}
} }
export default Installation; export default Installation;

View File

@ -1,25 +1,19 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import { FormControl, Grid, InputAdornment, TextField } from '@mui/material';
FormControl,
Grid,
InputAdornment,
TextField,
useTheme
} from '@mui/material';
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone'; import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
import FlatInstallationView from 'src/content/dashboards/Installations/FlatInstallationView'; import FlatInstallationView from 'src/content/dashboards/Installations/FlatInstallationView';
import { I_Installation } from '../../../interfaces/InstallationTypes'; 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 { interface installationSearchProps {
installations: I_Installation[]; installations: I_Installation[];
} }
function InstallationSearch(props: installationSearchProps) { function InstallationSearch(props: installationSearchProps) {
const theme = useTheme();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const searchParams = new URLSearchParams(location.search); const currentLocation = useLocation();
const installationId = parseInt(searchParams.get('installation'));
const [filteredData, setFilteredData] = useState(props.installations); const [filteredData, setFilteredData] = useState(props.installations);
useEffect(() => { useEffect(() => {
@ -38,7 +32,13 @@ function InstallationSearch(props: installationSearchProps) {
item item
xs={12} xs={12}
md={6} 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"> <FormControl variant="outlined">
<TextField <TextField
@ -59,6 +59,23 @@ function InstallationSearch(props: installationSearchProps) {
</Grid> </Grid>
<FlatInstallationView installations={filteredData} /> <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 React, { ChangeEvent, useContext, useEffect, useState } from 'react';
import Footer from 'src/components/Footer'; 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 ListIcon from '@mui/icons-material/List';
import AccountTreeIcon from '@mui/icons-material/AccountTree'; import AccountTreeIcon from '@mui/icons-material/AccountTree';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper'; import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import { import { Link, Navigate, Route, Routes, useLocation } from 'react-router-dom';
Link,
Route,
Routes,
useLocation,
useNavigate
} from 'react-router-dom';
import TreeView from '../Tree/treeView'; import TreeView from '../Tree/treeView';
import routes from 'src/Resources/routes.json'; import routes from 'src/Resources/routes.json';
import InstallationSearch from './InstallationSearch'; import InstallationSearch from './InstallationSearch';
@ -21,23 +15,42 @@ import Installation from './Installation';
import { WebSocketContext } from '../../../contexts/WebSocketContextProvider'; import { WebSocketContext } from '../../../contexts/WebSocketContextProvider';
function InstallationTabs() { function InstallationTabs() {
const theme = useTheme();
const location = useLocation(); 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 context = useContext(UserContext);
const { currentUser, setUser } = context; const { currentUser } = context;
const [currentTab, setCurrentTab] = useState<string>(searchParams.get('tab')); const tabList = [
'live',
'overview',
'manage',
'batteryview',
'log',
'information',
'configuration'
];
const [currentTab, setCurrentTab] = useState<string>(undefined);
const { installations, fetchAllInstallations } = const { installations, fetchAllInstallations } =
useContext(InstallationsContext); useContext(InstallationsContext);
const webSocketsContext = useContext(WebSocketContext); const webSocketsContext = useContext(WebSocketContext);
const { socket, openSocket } = webSocketsContext; 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(() => { useEffect(() => {
if (!socket && installations.length > 0) { if (!socket && installations.length > 0) {
openSocket(installations); openSocket(installations);
@ -48,51 +61,7 @@ function InstallationTabs() {
if (installations.length === 0) { if (installations.length === 0) {
fetchAllInstallations(); fetchAllInstallations();
} }
}, [installations]);
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]);
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => { const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value); setCurrentTab(value);
@ -164,64 +133,115 @@ function InstallationTabs() {
} }
]; ];
const tabs = installationId const tabs =
? currentUser.hasWriteAccess currentTab != 'list' &&
? [ currentTab != 'tree' &&
{ !location.pathname.includes('folder')
value: 'list', ? currentUser.hasWriteAccess
icon: <ListIcon id="mode-toggle-button-list-icon" /> ? [
}, {
{ value: 'list',
value: 'tree', icon: <ListIcon id="mode-toggle-button-list-icon" />
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" /> },
}, {
{ value: 'tree',
value: 'live', icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
label: <FormattedMessage id="live" defaultMessage="Live" /> },
}, {
{ value: 'live',
value: 'overview', label: <FormattedMessage id="live" defaultMessage="Live" />
label: <FormattedMessage id="overview" defaultMessage="Overview" /> },
}, {
{ value: 'overview',
value: 'batteryview', label: (
label: ( <FormattedMessage id="overview" defaultMessage="Overview" />
<FormattedMessage )
id="batteryview" },
defaultMessage="Battery View" {
/> value: 'batteryview',
) label: (
}, <FormattedMessage
{ id="batteryview"
value: 'manage', defaultMessage="Battery View"
label: ( />
<FormattedMessage )
id="manage" },
defaultMessage="Access Management" {
/> value: 'manage',
) label: (
}, <FormattedMessage
{ id="manage"
value: 'log', defaultMessage="Access Management"
label: <FormattedMessage id="log" defaultMessage="Log" /> />
}, )
{ },
value: 'information', {
label: ( value: 'log',
<FormattedMessage id="information" defaultMessage="Information" /> label: <FormattedMessage id="log" defaultMessage="Log" />
) },
}, {
value: 'information',
label: (
<FormattedMessage
id="information"
defaultMessage="Information"
/>
)
},
{ {
value: 'configuration', value: 'configuration',
label: ( label: (
<FormattedMessage <FormattedMessage
id="configuration" id="configuration"
defaultMessage="Configuration" defaultMessage="Configuration"
/> />
) )
} }
] ]
: [
{
value: 'list',
icon: <ListIcon id="mode-toggle-button-list-icon" />
},
{
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
},
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: (
<FormattedMessage id="overview" defaultMessage="Overview" />
)
},
{
value: 'batteryview',
label: (
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
},
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'information',
label: (
<FormattedMessage
id="information"
defaultMessage="Information"
/>
)
}
]
: [ : [
{ {
value: 'list', value: 'list',
@ -230,46 +250,8 @@ function InstallationTabs() {
{ {
value: 'tree', value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" /> icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
},
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
{
value: 'batteryview',
label: (
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
},
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'information',
label: (
<FormattedMessage id="information" defaultMessage="Information" />
)
} }
] ];
: [
{
value: 'list',
icon: <ListIcon id="mode-toggle-button-list-icon" />
},
{
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
}
];
return installations.length > 1 ? ( return installations.length > 1 ? (
<> <>
@ -293,7 +275,10 @@ function InstallationTabs() {
to={ to={
tab.value === 'list' || tab.value === 'tree' tab.value === 'list' || tab.value === 'tree'
? routes[tab.value] ? 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={routes.tree + '*'} element={<TreeView />} />
<Route
path={'*'}
element={
<Navigate to={routes.installations + routes.list}></Navigate>
}
></Route>
</Routes> </Routes>
</Grid> </Grid>
</Card> </Card>
@ -343,9 +334,12 @@ function InstallationTabs() {
value={tab.value} value={tab.value}
component={Link} component={Link}
label={tab.label} label={tab.label}
to={`?installation=${installations[0].id}&tab=${ to={
routes[tab.value] location.pathname.substring(
}`} 0,
location.pathname.lastIndexOf('/') + 1
) + routes[tab.value]
}
/> />
))} ))}
</Tabs> </Tabs>

View File

@ -1,5 +1,4 @@
import { DataPoint, DataRecord } from 'src/dataCache/data'; import { DataPoint, DataRecord } from 'src/dataCache/data';
import { TimeRange, UnixTime } from 'src/dataCache/time';
export interface I_CsvEntry { export interface I_CsvEntry {
value: string | number; value: string | number;
@ -100,9 +99,9 @@ export const topologyPaths: TopologyPaths = {
gridToAcInConnection: ['/GridMeter/Ac/Power/Active'], gridToAcInConnection: ['/GridMeter/Ac/Power/Active'],
gridBus: [ gridBus: [
'/GridMeter/Ac/L1/Power/Active', '/GridMeter/Ac/L1/Voltage',
'/GridMeter/Ac/L2/Power/Active', '/GridMeter/Ac/L2/Voltage',
'/GridMeter/Ac/L3/Power/Active' '/GridMeter/Ac/L3/Voltage'
], ],
gridBusToPvOnGridbusConnection: ['/PvOnAcGrid/Power/Active'], gridBusToPvOnGridbusConnection: ['/PvOnAcGrid/Power/Active'],
@ -110,19 +109,19 @@ export const topologyPaths: TopologyPaths = {
gridBusToIslandBusConnection: ['/AcGridToAcIsland/Power/Active'], gridBusToIslandBusConnection: ['/AcGridToAcIsland/Power/Active'],
islandBus: [ islandBus: [
'/AcDc/Ac/L1/Power/Active', '/AcDc/Ac/L1/Voltage',
'/AcDc/Ac/L2/Power/Active', '/AcDc/Ac/L2/Voltage',
'/AcDc/Ac/L3/Power/Active' '/AcDc/Ac/L3/Voltage'
], ],
islandBusToLoadOnIslandBusConnection: ['/LoadOnAcIsland/Ac/Power/Active'], islandBusToLoadOnIslandBusConnection: ['/LoadOnAcIsland/Ac/Power/Active'],
islandBusToInverter: ['/AcDc/Dc/Power'], islandBusToInverter: ['/AcDc/Dc/Power'],
pvOnIslandBusToIslandBusConnection: ['/PvOnAcIsland/Power/Active'], pvOnIslandBusToIslandBusConnection: ['/PvOnAcIsland/Power/Active'],
inverter: [ inverter: [
'/AcDc/Devices/1/Status/Ac/Power/Active', '/AcDc/Ac/L1/Power/Active',
'/AcDc/Devices/2/Status/Ac/Power/Active', '/AcDc/Ac/L2/Power/Active',
'/AcDc/Devices/3/Status/Ac/Power/Active', '/AcDc/Ac/L3/Power/Active',
'/AcDc/Devices/4/Status/Ac/Power/Active' '/AcDc/Ac/L4/Power/Active'
], ],
inverterToDcBus: ['/AcDcToDcLink/Power'], inverterToDcBus: ['/AcDcToDcLink/Power'],
@ -256,7 +255,9 @@ export const extractValues = (
for (const path of paths) { for (const path of paths) {
if (timeSeriesData.value.hasOwnProperty(path)) { if (timeSeriesData.value.hasOwnProperty(path)) {
topologyValues.push({ 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 value: timeSeriesData.value[path].value
}); });
} }
@ -302,19 +303,3 @@ export const getAmount = (
(Math.abs(values[0].value as number) / highestConnectionValue).toFixed(1) (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: { style: {
fontSize: '12px' fontSize: '12px'
}, },
offsetY: -190, offsetY: -185,
offsetX: 25, offsetX: 25,
rotate: 0 rotate: 0
}, },
@ -182,6 +182,7 @@ export const getChartOptions = (
}, },
tooltip: { tooltip: {
shared: true,
x: { x: {
format: 'dd MMM HH:mm:ss' format: 'dd MMM HH:mm:ss'
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,79 +1,47 @@
import React, { ChangeEvent, useContext, useEffect, useState } from 'react'; import React, { ChangeEvent, useContext, useEffect, useState } from 'react';
import { import { Card, Grid, Tab, Tabs } from '@mui/material';
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 { I_Folder } from 'src/interfaces/InstallationTypes'; 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 { UserContext } from 'src/contexts/userContext';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper'; import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import AccessContextProvider from 'src/contexts/AccessContextProvider'; import AccessContextProvider from 'src/contexts/AccessContextProvider';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import Access from '../ManageAccess/Access'; import Access from '../ManageAccess/Access';
import { FormattedMessage } from 'react-intl'; 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 { interface singleFolderProps {
current_folder: I_Folder; current_folder: I_Folder;
} }
function Folder(props: singleFolderProps) { function Folder(props: singleFolderProps) {
const theme = useTheme(); const [currentTab, setCurrentTab] = useState<string>(undefined);
const [currentTab, setCurrentTab] = useState<string>('folder');
const [formValues, setFormValues] = useState(props.current_folder); const [formValues, setFormValues] = useState(props.current_folder);
const [openModalFolder, setOpenModalFolder] = useState(false); const location = useLocation();
const [openModalInstallation, setOpenModalInstallation] = useState(false);
const requiredFields = ['name'];
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const context = useContext(UserContext); const context = useContext(UserContext);
const { currentUser, setUser } = context; const { currentUser } = 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 installationContext = useContext(InstallationsContext); const installationContext = useContext(InstallationsContext);
const { const { setError } = installationContext;
loading,
setLoading,
error,
setError,
updated,
setUpdated,
updateFolder,
deleteFolder
} = installationContext;
useEffect(() => { useEffect(() => {
setFormValues(props.current_folder); setFormValues(props.current_folder);
}, [props.current_folder]); }, [props.current_folder]);
useEffect(() => {
let path = location.pathname.split('/');
setCurrentTab(path[path.length - 1]);
}, [location]);
if (formValues == undefined) { if (formValues == undefined) {
return null; return null;
} }
const tabs = [ const tabs = [
{ {
value: 'folder', value: 'information',
label: <FormattedMessage id="folder" defaultMessage="Folder" /> label: <FormattedMessage id="information" defaultMessage="Information" />
}, },
{ {
value: 'manage', value: 'manage',
@ -88,394 +56,70 @@ function Folder(props: singleFolderProps) {
setError(false); setError(false);
}; };
const handleChange = (e) => { return (
const { name, value } = e.target; <>
setFormValues({ <Grid item xs={12} md={12}>
...formValues, <TabsContainerWrapper>
[name]: value <Tabs
}); onChange={handleTabsChange}
}; value={currentTab}
variant="scrollable"
const handleSelectOneUser = (installationID: number): void => { scrollButtons="auto"
if (selectedUser != installationID) { textColor="primary"
setSelectedUser(installationID); indicatorColor="primary"
} 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 {tabs.map((tab) => (
sx={{ <Tab
position: 'absolute', key={tab.value}
top: '50%', label={tab.label}
left: '50%', value={tab.value}
transform: 'translate(-50%, -50%)', component={Link}
width: 350, to={
bgcolor: 'background.paper', location.pathname.substring(
borderRadius: 4, 0,
boxShadow: 24, location.pathname.lastIndexOf('/') + 1
p: 4, ) + routes[tab.value]
display: 'flex', }
flexDirection: 'column', />
alignItems: 'center' ))}
}} </Tabs>
> </TabsContainerWrapper>
<Typography <Card variant="outlined">
variant="body1" <Grid
gutterBottom container
sx={{ fontWeight: 'bold' }} direction="row"
> justifyContent="center"
Do you want to delete this folder? alignItems="stretch"
</Typography> spacing={0}
<Typography >
variant="body1" <Routes>
gutterBottom <Route
sx={{ fontSize: '0.875rem' }} path={routes.information}
> element={
All installations of this folder will be deleted. <TreeInformation
</Typography> folder={props.current_folder}
></TreeInformation>
<div }
style={{ />
display: 'flex', {currentUser.hasWriteAccess && (
alignItems: 'center', <Route
marginTop: 10 path={routes.manage}
}} element={
> <AccessContextProvider>
<Button <Access
sx={{ currentResource={formValues}
marginTop: 2, resourceType="folder"
textTransform: 'none', ></Access>
bgcolor: '#ffc04d', </AccessContextProvider>
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
onChange={handleTabsChange}
value={currentTab}
variant="scrollable"
scrollButtons="auto"
textColor="primary"
indicatorColor="primary"
>
{tabs.map((tab) => (
<Tab key={tab.value} label={tab.label} value={tab.value} />
))}
</Tabs>
</TabsContainerWrapper>
<Card variant="outlined">
<Grid
container
direction="row"
justifyContent="center"
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"
/>
}
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 && ( </Routes>
<AccessContextProvider> </Grid>
<Access </Card>
currentResource={formValues} </Grid>
resourceType="folder" </>
></Access> );
</AccessContextProvider>
)}
</Grid>
</Card>
</Grid>
</>
);
} else {
return null;
}
} }
export default Folder; 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 ChevronRightIcon from '@mui/icons-material/ChevronRight';
import CustomTreeItem from './CustomTreeItem'; import CustomTreeItem from './CustomTreeItem';
import Installation from '../Installations/Installation'; import Installation from '../Installations/Installation';
import Folder from './Folder';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; 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() { function InstallationTree() {
const { foldersAndInstallations, fetchAllFoldersAndInstallations } = const { foldersAndInstallations, fetchAllFoldersAndInstallations } =
@ -65,24 +67,38 @@ function InstallationTree() {
</TreeView> </TreeView>
</Grid> </Grid>
{foldersAndInstallations.map((installation) => { <Routes>
if (installation.type == 'Installation') { {foldersAndInstallations.map((installation) => {
return ( if (installation.type == 'Installation') {
<Installation return (
key={installation.id + installation.type} <Route
current_installation={installation} key={installation.id}
type="tree" path={routes.installation + installation.id + '*'}
></Installation> element={
); <Installation
} else { key={installation.id}
return ( current_installation={installation}
<Folder type="installation"
key={installation.id + installation.type} ></Installation>
current_folder={installation} }
></Folder> />
); );
} } else {
})} return (
<Route
key={installation.id}
path={routes.folder + installation.id + '*'}
element={
<Folder
key={installation.id + installation.type}
current_folder={installation}
></Folder>
}
/>
);
}
})}
</Routes>
</Grid> </Grid>
); );
} }

View File

@ -14,7 +14,6 @@ import {
} from '@mui/material'; } from '@mui/material';
import { InnovEnergyUser } from 'src/interfaces/UserTypes'; import { InnovEnergyUser } from 'src/interfaces/UserTypes';
import User from './User'; import User from './User';
import { useNavigate } from 'react-router-dom';
interface FlatUsersViewProps { interface FlatUsersViewProps {
users: InnovEnergyUser[]; users: InnovEnergyUser[];
@ -23,15 +22,10 @@ interface FlatUsersViewProps {
const FlatUsersView = (props: FlatUsersViewProps) => { const FlatUsersView = (props: FlatUsersViewProps) => {
const [selectedUser, setSelectedUser] = useState<number>(-1); const [selectedUser, setSelectedUser] = useState<number>(-1);
const selectedBulkActions = selectedUser !== -1;
const navigate = useNavigate();
const handleSelectOneUser = (installationID: number): void => { const handleSelectOneUser = (userID: number): void => {
if (selectedUser != installationID) { if (selectedUser != userID) {
setSelectedUser(installationID); setSelectedUser(userID);
// navigate(routes.users + '?user=' + installationID.toString(), {
// replace: true
// });
} else { } else {
setSelectedUser(-1); setSelectedUser(-1);
} }

View File

@ -7,6 +7,13 @@ import { FetchResult } from '../dataCache/dataCache';
import { I_S3Credentials } from './S3Types'; import { I_S3Credentials } from './S3Types';
import { UnixTime } from '../dataCache/time'; import { UnixTime } from '../dataCache/time';
export interface chartInfoInterface {
magnitude: number;
unit: string;
min: number;
max: number;
}
export interface overviewInterface { export interface overviewInterface {
soc: chartInfoInterface; soc: chartInfoInterface;
temperature: chartInfoInterface; temperature: chartInfoInterface;
@ -18,13 +25,6 @@ export interface overviewInterface {
overview: chartInfoInterface; overview: chartInfoInterface;
} }
export interface chartInfoInterface {
magnitude: number;
unit: string;
min: number;
max: number;
}
export interface chartAggregatedDataInterface { export interface chartAggregatedDataInterface {
minsoc: { name: string; data: number[] }; minsoc: { name: string; data: number[] };
maxsoc: { name: string; data: number[] }; maxsoc: { name: string; data: number[] };
@ -45,6 +45,197 @@ export interface chartDataInterface {
dcBusVoltage: { name: string; data: number[] }; 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 ( export const transformInputToDailyData = async (
s3Credentials: I_S3Credentials, s3Credentials: I_S3Credentials,
startTimestamp: UnixTime, startTimestamp: UnixTime,
@ -53,8 +244,6 @@ export const transformInputToDailyData = async (
chartData: chartDataInterface; chartData: chartDataInterface;
chartOverview: overviewInterface; chartOverview: overviewInterface;
}> => { }> => {
const data = {};
const overviewData = {};
const prefixes = ['', 'k', 'M', 'G', 'T']; const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999; const MAX_NUMBER = 9999999;
const pathsToSearch = [ const pathsToSearch = [
@ -65,6 +254,14 @@ export const transformInputToDailyData = async (
'/PvOnDc/Dc/Power', '/PvOnDc/Dc/Power',
'/DcDc/Dc/Link/Voltage' '/DcDc/Dc/Link/Voltage'
]; ];
const categories = [
'soc',
'temperature',
'dcPower',
'gridPower',
'pvProduction',
'dcBusVoltage'
];
const chartData: chartDataInterface = { const chartData: chartDataInterface = {
soc: { name: 'State Of Charge', data: [] }, soc: { name: 'State Of Charge', data: [] },
@ -86,9 +283,9 @@ export const transformInputToDailyData = async (
overview: { magnitude: 0, unit: '', min: 0, max: 0 } overview: { magnitude: 0, unit: '', min: 0, max: 0 }
}; };
pathsToSearch.forEach((path) => { categories.forEach((category) => {
data[path] = []; chartData[category].data = [];
overviewData[path] = { chartOverview[category] = {
magnitude: 0, magnitude: 0,
unit: '', unit: '',
min: MAX_NUMBER, min: MAX_NUMBER,
@ -130,29 +327,33 @@ export const transformInputToDailyData = async (
// Handle not available or try later case // Handle not available or try later case
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-loop-func // eslint-disable-next-line @typescript-eslint/no-loop-func
let category_index = 0;
pathsToSearch.forEach((path) => { pathsToSearch.forEach((path) => {
if (result[path]) { if (result[path]) {
const value = result[path]; const value = result[path];
if (value.value < overviewData[path].min) { if (value.value < chartOverview[categories[category_index]].min) {
overviewData[path].min = value.value; chartOverview[categories[category_index]].min = value.value;
} }
if (value.value > overviewData[path].max) { if (value.value > chartOverview[categories[category_index]].max) {
overviewData[path].max = value.value; chartOverview[categories[category_index]].max = value.value;
} }
chartData[categories[category_index]].data.push([
data[path].push([adjustedTimestampArray[i], value.value]); adjustedTimestampArray[i],
value.value
]);
} else { } else {
//data[path].push([adjustedTimestamp, null]); //data[path].push([adjustedTimestamp, null]);
} }
category_index++;
}); });
} }
} }
pathsToSearch.forEach((path) => { categories.forEach((category) => {
let value = Math.max( let value = Math.max(
Math.abs(overviewData[path].max), Math.abs(chartOverview[category].max),
Math.abs(overviewData[path].min) Math.abs(chartOverview[category].min)
); );
let magnitude = 0; let magnitude = 0;
@ -163,81 +364,35 @@ export const transformInputToDailyData = async (
value /= 1000; value /= 1000;
magnitude++; magnitude++;
} }
overviewData[path].magnitude = magnitude; chartOverview[category].magnitude = magnitude;
}); });
let path = '/Battery/Soc'; chartOverview.soc.unit = '(%)';
chartData.soc.data = data[path]; chartOverview.soc.min = 0;
chartOverview.soc.max = 100;
chartOverview.soc = { chartOverview.temperature.unit = '(°C)';
unit: '(%)', chartOverview.dcPower.unit =
magnitude: overviewData[path].magnitude, '(' + prefixes[chartOverview['dcPower'].magnitude] + 'W' + ')';
min: 0, chartOverview.gridPower.unit =
max: 100 '(' + prefixes[chartOverview['gridPower'].magnitude] + 'W' + ')';
}; chartOverview.pvProduction.unit =
'(' + prefixes[chartOverview['pvProduction'].magnitude] + 'W' + ')';
path = '/Battery/Temperature'; chartOverview.dcBusVoltage.unit =
chartData.temperature.data = data[path]; '(' + prefixes[chartOverview['dcBusVoltage'].magnitude] + 'V' + ')';
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.overview = { chartOverview.overview = {
magnitude: Math.max( magnitude: Math.max(
overviewData['/GridMeter/Ac/Power/Active'].magnitude, chartOverview['gridPower'].magnitude,
overviewData['/PvOnDc/Dc/Power'].magnitude chartOverview['pvProduction'].magnitude
), ),
unit: '(kW)', unit: '(kW)',
min: Math.min( min: Math.min(
overviewData['/GridMeter/Ac/Power/Active'].min, chartOverview['gridPower'].min,
overviewData['/PvOnDc/Dc/Power'].min chartOverview['pvProduction'].min
), ),
max: Math.max( max: Math.max(
overviewData['/GridMeter/Ac/Power/Active'].max, chartOverview['gridPower'].max,
overviewData['/PvOnDc/Dc/Power'].max chartOverview['pvProduction'].max
) )
}; };
@ -277,6 +432,17 @@ export const transformInputToAggregatedData = async (
'/HeatingPower' '/HeatingPower'
]; ];
const categories = [
'minsoc',
'maxsoc',
'pvProduction',
'dcChargingPower',
'heatingPower',
'dcDischargingPower',
'gridImportPower',
'gridExportPower'
];
const chartAggregatedData: chartAggregatedDataInterface = { const chartAggregatedData: chartAggregatedDataInterface = {
minsoc: { name: 'min SOC', data: [] }, minsoc: { name: 'min SOC', data: [] },
maxsoc: { name: 'max 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/"
}