Updated battery view, fixed frontend routes,
This commit is contained in:
parent
aa0f33fcc6
commit
152efcdc15
|
@ -299,6 +299,7 @@ internal static class Program
|
|||
{
|
||||
subscribedNow = true;
|
||||
_subscribeToQueueForTheFirstTime = true;
|
||||
_prevSalimaxState = currentSalimaxState.Status;
|
||||
_subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,8 +25,7 @@ function App() {
|
|||
const context = useContext(UserContext);
|
||||
const { currentUser, setUser } = context;
|
||||
const tokencontext = useContext(TokenContext);
|
||||
const { token, setNewToken, removeToken } = tokencontext;
|
||||
const [forgotPassword, setForgotPassword] = useState(false);
|
||||
const { token, setNewToken } = tokencontext;
|
||||
const navigate = useNavigate();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const username = searchParams.get('username');
|
||||
|
@ -43,14 +42,6 @@ function App() {
|
|||
}
|
||||
};
|
||||
|
||||
const onForgotPassword = () => {
|
||||
setForgotPassword(true);
|
||||
};
|
||||
|
||||
const resetPassword = () => {
|
||||
setForgotPassword(false);
|
||||
};
|
||||
|
||||
const Loader = (Component) => (props) =>
|
||||
(
|
||||
<Suspense fallback={<SuspenseLoader />}>
|
||||
|
@ -58,11 +49,6 @@ function App() {
|
|||
</Suspense>
|
||||
);
|
||||
|
||||
// Dashboards
|
||||
const Installations = Loader(
|
||||
lazy(() => import('src/content/dashboards/Installations/'))
|
||||
);
|
||||
|
||||
const ResetPassword = Loader(
|
||||
lazy(() => import('src/components/ResetPassword'))
|
||||
);
|
||||
|
@ -84,23 +70,9 @@ function App() {
|
|||
navigate(routes.installations);
|
||||
}
|
||||
})
|
||||
.catch((error) => {});
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
// Status
|
||||
const Status404 = Loader(
|
||||
lazy(() => import('src/content/pages/Status/Status404'))
|
||||
);
|
||||
const Status500 = Loader(
|
||||
lazy(() => import('src/content/pages/Status/Status500'))
|
||||
);
|
||||
const StatusComingSoon = Loader(
|
||||
lazy(() => import('src/content/pages/Status/ComingSoon'))
|
||||
);
|
||||
const StatusMaintenance = Loader(
|
||||
lazy(() => import('src/content/pages/Status/Maintenance'))
|
||||
);
|
||||
|
||||
if (username) {
|
||||
loginToResetPassword();
|
||||
}
|
||||
|
@ -174,7 +146,6 @@ function App() {
|
|||
</AccessContextProvider>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path={routes.users + '*'} element={<Users />} />
|
||||
<Route
|
||||
path={'*'}
|
||||
|
|
|
@ -18,5 +18,7 @@
|
|||
"information": "information",
|
||||
"configuration": "configuration",
|
||||
"login": "/login/",
|
||||
"forgotPassword": "/forgotPassword/"
|
||||
"forgotPassword": "/forgotPassword/",
|
||||
"mainstats": "mainstats",
|
||||
"general": "general"
|
||||
}
|
||||
|
|
|
@ -1,37 +1,47 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Container,
|
||||
Grid,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
useTheme
|
||||
TableRow
|
||||
} from '@mui/material';
|
||||
import { TopologyValues } from '../Log/graph.util';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Link,
|
||||
Route,
|
||||
Routes,
|
||||
useLocation,
|
||||
useNavigate
|
||||
} from 'react-router-dom';
|
||||
import Button from '@mui/material/Button';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||
import routes from '../../../Resources/routes.json';
|
||||
import MainStats from './MainStats';
|
||||
|
||||
interface BatteryViewProps {
|
||||
values: TopologyValues;
|
||||
s3Credentials: I_S3Credentials;
|
||||
}
|
||||
|
||||
function BatteryView(props: BatteryViewProps) {
|
||||
if (props.values === null) {
|
||||
return null;
|
||||
}
|
||||
const theme = useTheme();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const installationId = parseInt(searchParams.get('installation'));
|
||||
const currentTab = searchParams.get('tab');
|
||||
|
||||
const currentPath = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||
const numOfBatteries = props.values.batteryView.values[0].value
|
||||
.toString()
|
||||
.split(',').length;
|
||||
|
||||
const batteryData = [];
|
||||
let batteryId = 1;
|
||||
|
||||
// Use a for loop to generate battery data
|
||||
for (let index = 1; index <= numOfBatteries * 7; index += 7) {
|
||||
const battery = {
|
||||
|
@ -61,8 +71,68 @@ function BatteryView(props: BatteryViewProps) {
|
|||
batteryData.push(battery);
|
||||
}
|
||||
|
||||
const handleMainStatsButton = () => {
|
||||
navigate(routes.mainstats);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let path = currentPath.pathname.split('/');
|
||||
|
||||
setCurrentTab(path[path.length - 1]);
|
||||
}, [currentPath]);
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<>
|
||||
<Container maxWidth="xl">
|
||||
<Grid container>
|
||||
<Routes>
|
||||
<Route
|
||||
path={routes.mainstats}
|
||||
element={
|
||||
<MainStats s3Credentials={props.s3Credentials}></MainStats>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
{currentTab === 'batteryview' && (
|
||||
<Grid item xs={6} md={6}>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
backgroundColor: '#808080',
|
||||
color: '#000000',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="main_stats"
|
||||
defaultMessage="Battery View"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleMainStatsButton}
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
marginLeft: '20px',
|
||||
// backgroundColor: mainStatsData ? '#808080' : '#ffc04d',
|
||||
backgroundColor: '#ffc04d',
|
||||
color: '#000000',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="main_stats" defaultMessage="Main Stats" />
|
||||
</Button>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{currentTab === 'batteryview' && (
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{ marginTop: '20px', marginBottom: '20px' }}
|
||||
>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
|
@ -142,7 +212,9 @@ function BatteryView(props: BatteryViewProps) {
|
|||
width: '10%',
|
||||
textAlign: 'center',
|
||||
backgroundColor:
|
||||
battery.AverageTemperature > 270 ? '#FF033E' : '#32CD32 '
|
||||
battery.AverageTemperature > 270
|
||||
? '#FF033E'
|
||||
: '#32CD32 '
|
||||
}}
|
||||
>
|
||||
{battery.AverageTemperature}
|
||||
|
@ -153,7 +225,8 @@ function BatteryView(props: BatteryViewProps) {
|
|||
width: '20%',
|
||||
textAlign: 'center',
|
||||
padding: '8px',
|
||||
fontWeight: battery.Warnings !== '' ? 'bold' : 'inherit',
|
||||
fontWeight:
|
||||
battery.Warnings !== '' ? 'bold' : 'inherit',
|
||||
backgroundColor:
|
||||
battery.Warnings === '' ? 'inherit' : '#ff9900',
|
||||
color: battery.Warnings != '' ? 'black' : 'inherit'
|
||||
|
@ -165,10 +238,12 @@ function BatteryView(props: BatteryViewProps) {
|
|||
<Link
|
||||
style={{ color: 'black' }}
|
||||
to={
|
||||
'?installation=' +
|
||||
installationId +
|
||||
'&tab=log' +
|
||||
'&open=warning'
|
||||
currentPath.pathname.substring(
|
||||
0,
|
||||
currentPath.pathname.lastIndexOf('/') + 1
|
||||
) +
|
||||
routes.log +
|
||||
'?open=warning'
|
||||
}
|
||||
>
|
||||
Multiple Warnings
|
||||
|
@ -193,10 +268,12 @@ function BatteryView(props: BatteryViewProps) {
|
|||
<Link
|
||||
style={{ color: 'black' }}
|
||||
to={
|
||||
'?installation=' +
|
||||
installationId +
|
||||
'&tab=log' +
|
||||
'&open=error'
|
||||
currentPath.pathname.substring(
|
||||
0,
|
||||
currentPath.pathname.lastIndexOf('/') + 1
|
||||
) +
|
||||
routes.log +
|
||||
'?open=error'
|
||||
}
|
||||
>
|
||||
Multiple Alarms
|
||||
|
@ -210,6 +287,9 @@ function BatteryView(props: BatteryViewProps) {
|
|||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CircularProgress,
|
||||
|
@ -13,11 +13,11 @@ import {
|
|||
useTheme
|
||||
} from '@mui/material';
|
||||
import { I_Installation } from 'src/interfaces/InstallationTypes';
|
||||
import Installation from './Installation';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import routes from '../../../Resources/routes.json';
|
||||
|
||||
interface FlatInstallationViewProps {
|
||||
installations: I_Installation[];
|
||||
|
@ -28,31 +28,32 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
const webSocketContext = useContext(WebSocketContext);
|
||||
const { getStatus } = webSocketContext;
|
||||
const navigate = useNavigate();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const installationId = parseInt(searchParams.get('installation'));
|
||||
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
|
||||
const currentLocation = useLocation();
|
||||
|
||||
const handleSelectOneInstallation = (installationID: number): void => {
|
||||
if (selectedInstallation != installationID) {
|
||||
setSelectedInstallation(installationID);
|
||||
navigate(`?installation=${installationID}`, {
|
||||
setSelectedInstallation(-1);
|
||||
|
||||
navigate(
|
||||
routes.installations +
|
||||
routes.list +
|
||||
routes.installation +
|
||||
`${installationID}` +
|
||||
'/' +
|
||||
routes.live,
|
||||
{
|
||||
replace: true
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setSelectedInstallation(-1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedInstallation(installationId);
|
||||
}, [installationId]);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const findInstallation = (id: number) => {
|
||||
return props.installations.find((installation) => installation.id === id);
|
||||
};
|
||||
|
||||
const handleRowMouseEnter = (id: number) => {
|
||||
setHoveredRow(id);
|
||||
};
|
||||
|
@ -63,7 +64,16 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
|
||||
return (
|
||||
<Grid container spacing={1} sx={{ marginTop: 0.1 }}>
|
||||
<Grid item sx={{ display: !installationId ? 'block' : 'none' }}>
|
||||
<Grid
|
||||
item
|
||||
sx={{
|
||||
display:
|
||||
currentLocation.pathname === routes.installations + 'list' ||
|
||||
currentLocation.pathname === routes.installations + routes.list
|
||||
? 'block'
|
||||
: 'none'
|
||||
}}
|
||||
>
|
||||
<Card>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
|
@ -225,14 +235,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
</TableContainer>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{props.installations.map((installation) => (
|
||||
<Installation
|
||||
key={installation.id}
|
||||
current_installation={findInstallation(installation.id)}
|
||||
type="installation"
|
||||
></Installation>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,24 +1,7 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Grid,
|
||||
IconButton,
|
||||
Modal,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { Card, CircularProgress, Grid, Typography } from '@mui/material';
|
||||
import { I_Installation } from 'src/interfaces/InstallationTypes';
|
||||
import Button from '@mui/material/Button';
|
||||
import { TokenContext } from 'src/contexts/tokenContext';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
|
||||
import AccessContextProvider from 'src/contexts/AccessContextProvider';
|
||||
import Access from '../ManageAccess/Access';
|
||||
import Log from 'src/content/dashboards/Log/Log';
|
||||
|
@ -36,6 +19,9 @@ import Configuration from '../Configuration/Configuration';
|
|||
import { fetchData } from 'src/content/dashboards/Installations/fetchData';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import BatteryView from '../BatteryView/BatteryView';
|
||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import routes from '../../../Resources/routes.json';
|
||||
import Information from '../Information/Information';
|
||||
|
||||
interface singleInstallationProps {
|
||||
current_installation?: I_Installation;
|
||||
|
@ -43,80 +29,20 @@ interface singleInstallationProps {
|
|||
}
|
||||
|
||||
function Installation(props: singleInstallationProps) {
|
||||
const theme = useTheme();
|
||||
const [formValues, setFormValues] = useState(props.current_installation);
|
||||
const requiredFields = ['name', 'region', 'location', 'country'];
|
||||
const context = useContext(UserContext);
|
||||
const { currentUser, setUser } = context;
|
||||
const tokencontext = useContext(TokenContext);
|
||||
const { removeToken } = tokencontext;
|
||||
const installationContext = useContext(InstallationsContext);
|
||||
const {
|
||||
updateInstallation,
|
||||
loading,
|
||||
setLoading,
|
||||
error,
|
||||
setError,
|
||||
updated,
|
||||
setUpdated,
|
||||
deleteInstallation
|
||||
} = installationContext;
|
||||
|
||||
const { currentUser } = context;
|
||||
const location = useLocation();
|
||||
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
|
||||
const webSocketsContext = useContext(WebSocketContext);
|
||||
const { getStatus } = webSocketsContext;
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const installationId = parseInt(searchParams.get('installation'));
|
||||
const currentTab = searchParams.get('tab');
|
||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||
const [values, setValues] = useState<TopologyValues | null>(null);
|
||||
const [openModalDeleteInstallation, setOpenModalDeleteInstallation] =
|
||||
useState(false);
|
||||
|
||||
const status = getStatus(props.current_installation.id);
|
||||
|
||||
if (formValues == undefined) {
|
||||
if (props.current_installation == undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormValues({
|
||||
...formValues,
|
||||
[name]: value
|
||||
});
|
||||
};
|
||||
const handleSubmit = (e) => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
updateInstallation(formValues, props.type);
|
||||
};
|
||||
|
||||
const handleDelete = (e) => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
setOpenModalDeleteInstallation(true);
|
||||
};
|
||||
|
||||
const deleteInstallationModalHandle = (e) => {
|
||||
setOpenModalDeleteInstallation(false);
|
||||
deleteInstallation(formValues, props.type);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const deleteInstallationModalHandleCancel = (e) => {
|
||||
setOpenModalDeleteInstallation(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const areRequiredFieldsFilled = () => {
|
||||
for (const field of requiredFields) {
|
||||
if (!formValues[field]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const S3data = {
|
||||
s3Region: props.current_installation.s3Region,
|
||||
s3Provider: props.current_installation.s3Provider,
|
||||
|
@ -132,15 +58,10 @@ function Installation(props: singleInstallationProps) {
|
|||
|
||||
const fetchDataPeriodically = async () => {
|
||||
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
|
||||
const date = now.toDate();
|
||||
|
||||
try {
|
||||
const res = await fetchData(now, s3Credentials);
|
||||
|
||||
// if (!isMounted) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
if (res != FetchResult.notAvailable && res != FetchResult.tryLater) {
|
||||
setValues(
|
||||
extractValues({
|
||||
|
@ -166,18 +87,22 @@ function Installation(props: singleInstallationProps) {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let path = location.pathname.split('/');
|
||||
|
||||
setCurrentTab(path[path.length - 1]);
|
||||
}, [location]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
installationId == props.current_installation.id &&
|
||||
(currentTab == 'live' ||
|
||||
currentTab == 'live' ||
|
||||
currentTab == 'configuration' ||
|
||||
currentTab == 'batteryview')
|
||||
currentTab == 'batteryview'
|
||||
) {
|
||||
//let isMounted = true;
|
||||
setFormValues(props.current_installation);
|
||||
var interval;
|
||||
|
||||
if (currentTab == 'live' || currentTab == 'batteryview') {
|
||||
fetchDataPeriodically();
|
||||
interval = setInterval(fetchDataPeriodically, 2000);
|
||||
}
|
||||
if (currentTab == 'configuration') {
|
||||
|
@ -186,85 +111,15 @@ function Installation(props: singleInstallationProps) {
|
|||
|
||||
// Cleanup function to cancel interval and update isMounted when unmounted
|
||||
return () => {
|
||||
//isMounted = false;
|
||||
if (currentTab == 'live' || currentTab == 'batteryview') {
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [installationId, currentTab]);
|
||||
}, [currentTab, location.pathname]);
|
||||
|
||||
if (installationId == props.current_installation.id) {
|
||||
return (
|
||||
<>
|
||||
{openModalDeleteInstallation && (
|
||||
<Modal
|
||||
open={openModalDeleteInstallation}
|
||||
onClose={deleteInstallationModalHandleCancel}
|
||||
aria-labelledby="error-modal"
|
||||
aria-describedby="error-modal-description"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 350,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Do you want to delete this installation?
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
onClick={deleteInstallationModalHandle}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
marginLeft: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
onClick={deleteInstallationModalHandleCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<Grid item xs={12} md={12}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Typography
|
||||
|
@ -397,304 +252,83 @@ function Installation(props: singleInstallationProps) {
|
|||
alignItems="stretch"
|
||||
spacing={0}
|
||||
>
|
||||
{currentTab === 'information' && (
|
||||
<Container maxWidth="xl">
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
alignItems="stretch"
|
||||
spacing={3}
|
||||
>
|
||||
<Grid item xs={12} md={12}>
|
||||
<CardContent>
|
||||
<Box
|
||||
component="form"
|
||||
sx={{
|
||||
'& .MuiTextField-root': { m: 1, width: '50ch' }
|
||||
}}
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="customerName"
|
||||
defaultMessage="Customer Name"
|
||||
/>
|
||||
<Routes>
|
||||
<Route
|
||||
path={routes.information}
|
||||
element={
|
||||
<Information
|
||||
values={props.current_installation}
|
||||
s3Credentials={s3Credentials}
|
||||
type={props.type}
|
||||
></Information>
|
||||
}
|
||||
name="name"
|
||||
value={formValues.name}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
required
|
||||
error={formValues.name === ''}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="region"
|
||||
defaultMessage="Region"
|
||||
/>
|
||||
}
|
||||
name="region"
|
||||
value={formValues.region}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
required
|
||||
error={formValues.name === ''}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="location"
|
||||
defaultMessage="Location"
|
||||
/>
|
||||
}
|
||||
name="location"
|
||||
value={formValues.location}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
required
|
||||
error={formValues.name === ''}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="country"
|
||||
defaultMessage="Country"
|
||||
/>
|
||||
}
|
||||
name="country"
|
||||
value={formValues.country}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
required
|
||||
error={formValues.name === ''}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="orderNumbers"
|
||||
defaultMessage="Order Numbers"
|
||||
/>
|
||||
}
|
||||
name="orderNumbers"
|
||||
value={formValues.orderNumbers}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="installation_name"
|
||||
defaultMessage="Installation Name"
|
||||
/>
|
||||
}
|
||||
name="installationName"
|
||||
value={formValues.installationName}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
{currentUser.hasWriteAccess && (
|
||||
<>
|
||||
<div>
|
||||
<TextField
|
||||
label="Vpn IP"
|
||||
name="VpnIp"
|
||||
value={formValues.vpnIp}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label="S3 Write Key"
|
||||
name="s3writekey"
|
||||
value={formValues.s3WriteKey}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label="S3 Write Secret Key"
|
||||
name="s3writesecretkey"
|
||||
value={formValues.s3WriteSecret}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label="S3 Bucket Name"
|
||||
name="s3writesecretkey"
|
||||
value={
|
||||
formValues.id +
|
||||
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
|
||||
}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
{currentUser.hasWriteAccess && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleDelete}
|
||||
sx={{
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="deleteInstallation"
|
||||
defaultMessage="Delete Installation"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{currentUser.hasWriteAccess && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
sx={{
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
disabled={!areRequiredFieldsFilled()}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="applyChanges"
|
||||
defaultMessage="Apply Changes"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
marginLeft: '20px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{
|
||||
ml: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="errorOccured"
|
||||
defaultMessage="An error has occurred"
|
||||
/>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setError(false)}
|
||||
sx={{ marginLeft: '4px' }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
{updated && (
|
||||
<Alert
|
||||
severity="success"
|
||||
sx={{
|
||||
ml: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="successfullyUpdated"
|
||||
defaultMessage="Successfully updated"
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setUpdated(false)} // Set error state to false on click
|
||||
sx={{ marginLeft: '4px' }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
)}
|
||||
{currentTab === 'overview' && (
|
||||
<Overview s3Credentials={s3Credentials}></Overview>
|
||||
)}
|
||||
{currentTab === 'batteryview' && (
|
||||
<BatteryView values={values}></BatteryView>
|
||||
)}
|
||||
{currentTab === 'configuration' && currentUser.hasWriteAccess && (
|
||||
<Configuration
|
||||
<Route
|
||||
path={routes.batteryview + '*'}
|
||||
element={
|
||||
<BatteryView
|
||||
values={values}
|
||||
id={installationId}
|
||||
></Configuration>
|
||||
)}
|
||||
{currentTab === 'manage' && currentUser.hasWriteAccess && (
|
||||
<AccessContextProvider>
|
||||
<Access
|
||||
currentResource={formValues}
|
||||
resourceType={props.type}
|
||||
></Access>
|
||||
</AccessContextProvider>
|
||||
)}
|
||||
{currentTab === 'live' && <Topology values={values}></Topology>}
|
||||
{currentTab === 'log' && (
|
||||
s3Credentials={s3Credentials}
|
||||
></BatteryView>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={routes.overview}
|
||||
element={<Overview s3Credentials={s3Credentials}></Overview>}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={routes.live}
|
||||
element={<Topology values={values}></Topology>}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={routes.log}
|
||||
element={
|
||||
<Log
|
||||
errorLoadingS3Data={errorLoadingS3Data}
|
||||
id={props.current_installation.id}
|
||||
></Log>
|
||||
}
|
||||
/>
|
||||
|
||||
{currentUser.hasWriteAccess && (
|
||||
<Route
|
||||
path={routes.configuration}
|
||||
element={
|
||||
<Configuration
|
||||
values={values}
|
||||
id={props.current_installation.id}
|
||||
></Configuration>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{currentUser.hasWriteAccess && (
|
||||
<Route
|
||||
path={routes.manage}
|
||||
element={
|
||||
<AccessContextProvider>
|
||||
<Access
|
||||
currentResource={props.current_installation}
|
||||
resourceType={props.type}
|
||||
></Access>
|
||||
</AccessContextProvider>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Route
|
||||
path={'*'}
|
||||
element={<Navigate to={routes.live}></Navigate>}
|
||||
/>
|
||||
</Routes>
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default Installation;
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
FormControl,
|
||||
Grid,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { FormControl, Grid, InputAdornment, TextField } from '@mui/material';
|
||||
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
|
||||
import FlatInstallationView from 'src/content/dashboards/Installations/FlatInstallationView';
|
||||
import { I_Installation } from '../../../interfaces/InstallationTypes';
|
||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||
import routes from '../../../Resources/routes.json';
|
||||
import Installation from './Installation';
|
||||
|
||||
interface installationSearchProps {
|
||||
installations: I_Installation[];
|
||||
}
|
||||
|
||||
function InstallationSearch(props: installationSearchProps) {
|
||||
const theme = useTheme();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const installationId = parseInt(searchParams.get('installation'));
|
||||
|
||||
const currentLocation = useLocation();
|
||||
const [filteredData, setFilteredData] = useState(props.installations);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -38,7 +32,13 @@ function InstallationSearch(props: installationSearchProps) {
|
|||
item
|
||||
xs={12}
|
||||
md={6}
|
||||
sx={{ display: !installationId ? 'block' : 'none' }}
|
||||
sx={{
|
||||
display:
|
||||
currentLocation.pathname === routes.installations + 'list' ||
|
||||
currentLocation.pathname === routes.installations + routes.list
|
||||
? 'block'
|
||||
: 'none'
|
||||
}}
|
||||
>
|
||||
<FormControl variant="outlined">
|
||||
<TextField
|
||||
|
@ -59,6 +59,23 @@ function InstallationSearch(props: installationSearchProps) {
|
|||
</Grid>
|
||||
|
||||
<FlatInstallationView installations={filteredData} />
|
||||
<Routes>
|
||||
{filteredData.map((installation) => {
|
||||
return (
|
||||
<Route
|
||||
key={installation.id}
|
||||
path={routes.installation + installation.id + '*'}
|
||||
element={
|
||||
<Installation
|
||||
key={installation.id}
|
||||
current_installation={installation}
|
||||
type="installation"
|
||||
></Installation>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
import React, { ChangeEvent, useContext, useEffect, useState } from 'react';
|
||||
import Footer from 'src/components/Footer';
|
||||
import { Box, Card, Container, Grid, Tab, Tabs, useTheme } from '@mui/material';
|
||||
import { Box, Card, Container, Grid, Tab, Tabs } from '@mui/material';
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
|
||||
import {
|
||||
Link,
|
||||
Route,
|
||||
Routes,
|
||||
useLocation,
|
||||
useNavigate
|
||||
} from 'react-router-dom';
|
||||
import { Link, Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import TreeView from '../Tree/treeView';
|
||||
import routes from 'src/Resources/routes.json';
|
||||
import InstallationSearch from './InstallationSearch';
|
||||
|
@ -21,23 +15,42 @@ import Installation from './Installation';
|
|||
import { WebSocketContext } from '../../../contexts/WebSocketContextProvider';
|
||||
|
||||
function InstallationTabs() {
|
||||
const theme = useTheme();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const installationId = parseInt(searchParams.get('installation'));
|
||||
//const currentTab = searchParams.get('tab');
|
||||
const [singleInstallationID, setSingleInstallationID] = useState(-1);
|
||||
const context = useContext(UserContext);
|
||||
const { currentUser, setUser } = context;
|
||||
const [currentTab, setCurrentTab] = useState<string>(searchParams.get('tab'));
|
||||
const { currentUser } = context;
|
||||
const tabList = [
|
||||
'live',
|
||||
'overview',
|
||||
'manage',
|
||||
'batteryview',
|
||||
'log',
|
||||
'information',
|
||||
'configuration'
|
||||
];
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||
const { installations, fetchAllInstallations } =
|
||||
useContext(InstallationsContext);
|
||||
|
||||
const webSocketsContext = useContext(WebSocketContext);
|
||||
const { socket, openSocket } = webSocketsContext;
|
||||
|
||||
useEffect(() => {
|
||||
let path = location.pathname.split('/');
|
||||
|
||||
if (path[path.length - 2] === 'list') {
|
||||
setCurrentTab('list');
|
||||
} else if (path[path.length - 2] === 'tree') {
|
||||
setCurrentTab('tree');
|
||||
} else {
|
||||
setCurrentTab(
|
||||
tabList.includes(path[path.length - 1])
|
||||
? path[path.length - 1]
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket && installations.length > 0) {
|
||||
openSocket(installations);
|
||||
|
@ -48,51 +61,7 @@ function InstallationTabs() {
|
|||
if (installations.length === 0) {
|
||||
fetchAllInstallations();
|
||||
}
|
||||
|
||||
if (installations.length === 1) {
|
||||
if (!currentTab) {
|
||||
navigate(`?installation=${installations[0].id}&tab=live`, {
|
||||
replace: true
|
||||
});
|
||||
setCurrentTab('live');
|
||||
} else {
|
||||
navigate(`?installation=${installations[0].id}&tab=${currentTab}`, {
|
||||
replace: true
|
||||
});
|
||||
}
|
||||
} else if (installations.length > 1) {
|
||||
if (
|
||||
location.pathname === '/installations' ||
|
||||
location.pathname === '/installations/'
|
||||
) {
|
||||
navigate(routes.installations + routes.list, {
|
||||
replace: true
|
||||
});
|
||||
} else if (
|
||||
location.pathname === '/installations/tree/' &&
|
||||
!installationId
|
||||
) {
|
||||
setCurrentTab('tree');
|
||||
} else if (
|
||||
location.pathname === '/installations/list/' &&
|
||||
!installationId
|
||||
) {
|
||||
setCurrentTab('list');
|
||||
}
|
||||
if (installationId) {
|
||||
if (currentTab == 'list' || currentTab == 'tree') {
|
||||
navigate(`?installation=${installationId}&tab=live`, {
|
||||
replace: true
|
||||
});
|
||||
setCurrentTab('live');
|
||||
} else {
|
||||
navigate(`?installation=${installationId}&tab=${currentTab}`, {
|
||||
replace: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [location.pathname, navigate, installationId, installations]);
|
||||
}, [installations]);
|
||||
|
||||
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
|
||||
setCurrentTab(value);
|
||||
|
@ -164,7 +133,10 @@ function InstallationTabs() {
|
|||
}
|
||||
];
|
||||
|
||||
const tabs = installationId
|
||||
const tabs =
|
||||
currentTab != 'list' &&
|
||||
currentTab != 'tree' &&
|
||||
!location.pathname.includes('folder')
|
||||
? currentUser.hasWriteAccess
|
||||
? [
|
||||
{
|
||||
|
@ -181,7 +153,9 @@ function InstallationTabs() {
|
|||
},
|
||||
{
|
||||
value: 'overview',
|
||||
label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
label: (
|
||||
<FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'batteryview',
|
||||
|
@ -208,7 +182,10 @@ function InstallationTabs() {
|
|||
{
|
||||
value: 'information',
|
||||
label: (
|
||||
<FormattedMessage id="information" defaultMessage="Information" />
|
||||
<FormattedMessage
|
||||
id="information"
|
||||
defaultMessage="Information"
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
|
@ -238,7 +215,9 @@ function InstallationTabs() {
|
|||
},
|
||||
{
|
||||
value: 'overview',
|
||||
label: <FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
label: (
|
||||
<FormattedMessage id="overview" defaultMessage="Overview" />
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'batteryview',
|
||||
|
@ -256,7 +235,10 @@ function InstallationTabs() {
|
|||
{
|
||||
value: 'information',
|
||||
label: (
|
||||
<FormattedMessage id="information" defaultMessage="Information" />
|
||||
<FormattedMessage
|
||||
id="information"
|
||||
defaultMessage="Information"
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
@ -293,7 +275,10 @@ function InstallationTabs() {
|
|||
to={
|
||||
tab.value === 'list' || tab.value === 'tree'
|
||||
? routes[tab.value]
|
||||
: `?installation=${installationId}&tab=${routes[tab.value]}`
|
||||
: location.pathname.substring(
|
||||
0,
|
||||
location.pathname.lastIndexOf('/') + 1
|
||||
) + routes[tab.value]
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
@ -319,6 +304,12 @@ function InstallationTabs() {
|
|||
}
|
||||
/>
|
||||
<Route path={routes.tree + '*'} element={<TreeView />} />
|
||||
<Route
|
||||
path={'*'}
|
||||
element={
|
||||
<Navigate to={routes.installations + routes.list}></Navigate>
|
||||
}
|
||||
></Route>
|
||||
</Routes>
|
||||
</Grid>
|
||||
</Card>
|
||||
|
@ -343,9 +334,12 @@ function InstallationTabs() {
|
|||
value={tab.value}
|
||||
component={Link}
|
||||
label={tab.label}
|
||||
to={`?installation=${installations[0].id}&tab=${
|
||||
routes[tab.value]
|
||||
}`}
|
||||
to={
|
||||
location.pathname.substring(
|
||||
0,
|
||||
location.pathname.lastIndexOf('/') + 1
|
||||
) + routes[tab.value]
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { DataPoint, DataRecord } from 'src/dataCache/data';
|
||||
import { TimeRange, UnixTime } from 'src/dataCache/time';
|
||||
|
||||
export interface I_CsvEntry {
|
||||
value: string | number;
|
||||
|
@ -100,9 +99,9 @@ export const topologyPaths: TopologyPaths = {
|
|||
gridToAcInConnection: ['/GridMeter/Ac/Power/Active'],
|
||||
|
||||
gridBus: [
|
||||
'/GridMeter/Ac/L1/Power/Active',
|
||||
'/GridMeter/Ac/L2/Power/Active',
|
||||
'/GridMeter/Ac/L3/Power/Active'
|
||||
'/GridMeter/Ac/L1/Voltage',
|
||||
'/GridMeter/Ac/L2/Voltage',
|
||||
'/GridMeter/Ac/L3/Voltage'
|
||||
],
|
||||
gridBusToPvOnGridbusConnection: ['/PvOnAcGrid/Power/Active'],
|
||||
|
||||
|
@ -110,19 +109,19 @@ export const topologyPaths: TopologyPaths = {
|
|||
gridBusToIslandBusConnection: ['/AcGridToAcIsland/Power/Active'],
|
||||
|
||||
islandBus: [
|
||||
'/AcDc/Ac/L1/Power/Active',
|
||||
'/AcDc/Ac/L2/Power/Active',
|
||||
'/AcDc/Ac/L3/Power/Active'
|
||||
'/AcDc/Ac/L1/Voltage',
|
||||
'/AcDc/Ac/L2/Voltage',
|
||||
'/AcDc/Ac/L3/Voltage'
|
||||
],
|
||||
islandBusToLoadOnIslandBusConnection: ['/LoadOnAcIsland/Ac/Power/Active'],
|
||||
islandBusToInverter: ['/AcDc/Dc/Power'],
|
||||
pvOnIslandBusToIslandBusConnection: ['/PvOnAcIsland/Power/Active'],
|
||||
|
||||
inverter: [
|
||||
'/AcDc/Devices/1/Status/Ac/Power/Active',
|
||||
'/AcDc/Devices/2/Status/Ac/Power/Active',
|
||||
'/AcDc/Devices/3/Status/Ac/Power/Active',
|
||||
'/AcDc/Devices/4/Status/Ac/Power/Active'
|
||||
'/AcDc/Ac/L1/Power/Active',
|
||||
'/AcDc/Ac/L2/Power/Active',
|
||||
'/AcDc/Ac/L3/Power/Active',
|
||||
'/AcDc/Ac/L4/Power/Active'
|
||||
],
|
||||
inverterToDcBus: ['/AcDcToDcLink/Power'],
|
||||
|
||||
|
@ -256,7 +255,9 @@ export const extractValues = (
|
|||
for (const path of paths) {
|
||||
if (timeSeriesData.value.hasOwnProperty(path)) {
|
||||
topologyValues.push({
|
||||
unit: timeSeriesData.value[path].unit,
|
||||
unit: timeSeriesData.value[path].unit.includes('~')
|
||||
? timeSeriesData.value[path].unit.replace('~', '')
|
||||
: timeSeriesData.value[path].unit,
|
||||
value: timeSeriesData.value[path].value
|
||||
});
|
||||
}
|
||||
|
@ -302,19 +303,3 @@ export const getAmount = (
|
|||
(Math.abs(values[0].value as number) / highestConnectionValue).toFixed(1)
|
||||
);
|
||||
};
|
||||
|
||||
export const createTimes = (
|
||||
range: TimeRange,
|
||||
numberOfNodes: number
|
||||
): UnixTime[] => {
|
||||
const oneSpan = range.duration.divide(numberOfNodes);
|
||||
//console.log(oneSpan);
|
||||
|
||||
const roundedRange = TimeRange.fromTimes(
|
||||
range.start.round(oneSpan),
|
||||
range.end.round(oneSpan)
|
||||
);
|
||||
|
||||
const unixTimes = range.sample(oneSpan);
|
||||
return unixTimes;
|
||||
};
|
||||
|
|
|
@ -167,7 +167,7 @@ export const getChartOptions = (
|
|||
style: {
|
||||
fontSize: '12px'
|
||||
},
|
||||
offsetY: -190,
|
||||
offsetY: -185,
|
||||
offsetX: 25,
|
||||
rotate: 0
|
||||
},
|
||||
|
@ -182,6 +182,7 @@ export const getChartOptions = (
|
|||
},
|
||||
|
||||
tooltip: {
|
||||
shared: true,
|
||||
x: {
|
||||
format: 'dd MMM HH:mm:ss'
|
||||
},
|
||||
|
|
|
@ -41,7 +41,7 @@ const computeLast7Days = (): string[] => {
|
|||
|
||||
function Overview(props: OverviewProps) {
|
||||
const context = useContext(UserContext);
|
||||
const { currentUser, setUser } = context;
|
||||
const { currentUser } = context;
|
||||
|
||||
const [dailyData, setDailyData] = useState(true);
|
||||
const [weeklyData, setWeeklyData] = useState(false);
|
||||
|
@ -444,20 +444,20 @@ function Overview(props: OverviewProps) {
|
|||
>
|
||||
<FormattedMessage id="lastweek" defaultMessage="Last week" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleMonthData}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
marginLeft: '10px',
|
||||
backgroundColor: monthlyData ? '#808080' : '#ffc04d',
|
||||
color: '#000000',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="lastmonth" defaultMessage="Last Month" />
|
||||
</Button>
|
||||
{/*<Button*/}
|
||||
{/* variant="contained"*/}
|
||||
{/* onClick={handleMonthData}*/}
|
||||
{/* disabled={loading}*/}
|
||||
{/* sx={{*/}
|
||||
{/* marginTop: '20px',*/}
|
||||
{/* marginLeft: '10px',*/}
|
||||
{/* backgroundColor: monthlyData ? '#808080' : '#ffc04d',*/}
|
||||
{/* color: '#000000',*/}
|
||||
{/* '&:hover': { bgcolor: '#f7b34d' }*/}
|
||||
{/* }}*/}
|
||||
{/*>*/}
|
||||
{/* <FormattedMessage id="lastmonth" defaultMessage="Last Month" />*/}
|
||||
{/*</Button>*/}
|
||||
{dailyData && (
|
||||
<>
|
||||
<Button
|
||||
|
|
|
@ -57,17 +57,6 @@ function Topology(props: TopologyProps) {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/*<Switch*/}
|
||||
{/* edge="start"*/}
|
||||
{/* color="secondary"*/}
|
||||
{/* onChange={handleSwitch()}*/}
|
||||
{/* sx={{*/}
|
||||
{/* '& .MuiSwitch-thumb': {*/}
|
||||
{/* backgroundColor: 'orange'*/}
|
||||
{/* }*/}
|
||||
{/* }}*/}
|
||||
{/*/>*/}
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
|
|
|
@ -47,7 +47,7 @@ function formatPower(value, unit) {
|
|||
magnitude++;
|
||||
}
|
||||
|
||||
const roundedValue = Math.round(value);
|
||||
const roundedValue = value.toFixed(1);
|
||||
|
||||
//Filter all values less than 100 Watts
|
||||
if (magnitude === 0 && value < 100 && unit === 'W') {
|
||||
|
|
|
@ -45,7 +45,7 @@ function formatPower(value) {
|
|||
magnitude++;
|
||||
}
|
||||
|
||||
const roundedValue = Math.round(value);
|
||||
const roundedValue = value.toFixed(1);
|
||||
|
||||
//Filter all values less than 100 Watts
|
||||
if (magnitude === 0 && value < 100) {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { makeStyles } from '@mui/styles';
|
|||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import { WebSocketContext } from 'src/contexts/WebSocketContextProvider';
|
||||
import routes from 'src/Resources/routes.json';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
interface CustomTreeItemProps {
|
||||
node: I_Installation | I_Folder;
|
||||
|
@ -43,8 +43,7 @@ function CustomTreeItem(props: CustomTreeItemProps) {
|
|||
const status = getStatus(props.node.id);
|
||||
const navigate = useNavigate();
|
||||
const [selected, setSelected] = useState(false);
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const installationId = parseInt(searchParams.get('installation'));
|
||||
const currentLocation = useLocation();
|
||||
|
||||
const handleSelectOneInstallation = (): void => {
|
||||
let installation = props.node;
|
||||
|
@ -52,8 +51,10 @@ function CustomTreeItem(props: CustomTreeItemProps) {
|
|||
navigate(
|
||||
routes.installations +
|
||||
routes.tree +
|
||||
'?installation=' +
|
||||
installation.id.toString(),
|
||||
routes.installation +
|
||||
installation.id +
|
||||
'/' +
|
||||
routes.live,
|
||||
{
|
||||
replace: true
|
||||
}
|
||||
|
@ -63,8 +64,10 @@ function CustomTreeItem(props: CustomTreeItemProps) {
|
|||
navigate(
|
||||
routes.installations +
|
||||
routes.tree +
|
||||
'?folder=' +
|
||||
installation.id.toString(),
|
||||
routes.folder +
|
||||
installation.id +
|
||||
'/' +
|
||||
routes.information,
|
||||
{
|
||||
replace: true
|
||||
}
|
||||
|
@ -152,7 +155,12 @@ function CustomTreeItem(props: CustomTreeItemProps) {
|
|||
</div>
|
||||
}
|
||||
sx={{
|
||||
display: !installationId ? 'block' : 'none',
|
||||
display:
|
||||
currentLocation.pathname === routes.installations + 'tree' ||
|
||||
currentLocation.pathname === routes.installations + routes.tree ||
|
||||
currentLocation.pathname.includes('folder')
|
||||
? 'block'
|
||||
: 'none',
|
||||
'.MuiTreeItem-content': {
|
||||
width: 'inherit',
|
||||
|
||||
|
|
|
@ -1,79 +1,47 @@
|
|||
import React, { ChangeEvent, useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Grid,
|
||||
IconButton,
|
||||
Modal,
|
||||
Tab,
|
||||
Tabs,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { Card, Grid, Tab, Tabs } from '@mui/material';
|
||||
import { I_Folder } from 'src/interfaces/InstallationTypes';
|
||||
import Button from '@mui/material/Button';
|
||||
import FolderForm from './folderForm';
|
||||
import InstallationForm from '../Installations/installationForm';
|
||||
import { TokenContext } from 'src/contexts/tokenContext';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
|
||||
import AccessContextProvider from 'src/contexts/AccessContextProvider';
|
||||
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
|
||||
import Access from '../ManageAccess/Access';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import routes from '../../../Resources/routes.json';
|
||||
import TreeInformation from './Information';
|
||||
|
||||
interface singleFolderProps {
|
||||
current_folder: I_Folder;
|
||||
}
|
||||
|
||||
function Folder(props: singleFolderProps) {
|
||||
const theme = useTheme();
|
||||
const [currentTab, setCurrentTab] = useState<string>('folder');
|
||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||
const [formValues, setFormValues] = useState(props.current_folder);
|
||||
const [openModalFolder, setOpenModalFolder] = useState(false);
|
||||
const [openModalInstallation, setOpenModalInstallation] = useState(false);
|
||||
const requiredFields = ['name'];
|
||||
const tokencontext = useContext(TokenContext);
|
||||
const { removeToken } = tokencontext;
|
||||
const location = useLocation();
|
||||
const context = useContext(UserContext);
|
||||
const { currentUser, setUser } = context;
|
||||
const [isRowHovered, setHoveredRow] = useState(-1);
|
||||
const [selectedUser, setSelectedUser] = useState<number>(-1);
|
||||
const selectedBulkActions = selectedUser !== -1;
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const folderId = parseInt(searchParams.get('folder'));
|
||||
const [openModalDeleteFolder, setOpenModalDeleteFolder] = useState(false);
|
||||
|
||||
const { currentUser } = context;
|
||||
const installationContext = useContext(InstallationsContext);
|
||||
const {
|
||||
loading,
|
||||
setLoading,
|
||||
error,
|
||||
setError,
|
||||
updated,
|
||||
setUpdated,
|
||||
updateFolder,
|
||||
deleteFolder
|
||||
} = installationContext;
|
||||
const { setError } = installationContext;
|
||||
|
||||
useEffect(() => {
|
||||
setFormValues(props.current_folder);
|
||||
}, [props.current_folder]);
|
||||
|
||||
useEffect(() => {
|
||||
let path = location.pathname.split('/');
|
||||
|
||||
setCurrentTab(path[path.length - 1]);
|
||||
}, [location]);
|
||||
|
||||
if (formValues == undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
value: 'folder',
|
||||
label: <FormattedMessage id="folder" defaultMessage="Folder" />
|
||||
value: 'information',
|
||||
label: <FormattedMessage id="information" defaultMessage="Information" />
|
||||
},
|
||||
{
|
||||
value: 'manage',
|
||||
|
@ -88,175 +56,8 @@ function Folder(props: singleFolderProps) {
|
|||
setError(false);
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormValues({
|
||||
...formValues,
|
||||
[name]: value
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectOneUser = (installationID: number): void => {
|
||||
if (selectedUser != installationID) {
|
||||
setSelectedUser(installationID);
|
||||
} else {
|
||||
setSelectedUser(-1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRowMouseEnter = (id: number) => {
|
||||
setHoveredRow(id);
|
||||
};
|
||||
|
||||
const handleRowMouseLeave = () => {
|
||||
setHoveredRow(-1);
|
||||
};
|
||||
|
||||
const handleFolderInformationUpdate = (e) => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
updateFolder(formValues);
|
||||
};
|
||||
|
||||
const handleNewInstallationInsertion = (e) => {
|
||||
setOpenModalInstallation(true);
|
||||
};
|
||||
|
||||
const handleNewFolderInsertion = (e) => {
|
||||
setOpenModalFolder(true);
|
||||
};
|
||||
|
||||
const handleDeleteFolder = (e) => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
setOpenModalDeleteFolder(true);
|
||||
};
|
||||
|
||||
const deleteFolderModalHandle = (e) => {
|
||||
setOpenModalDeleteFolder(false);
|
||||
deleteFolder(formValues);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const deleteFolderModalHandleCancel = (e) => {
|
||||
setOpenModalDeleteFolder(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleFolderFormSubmit = () => {
|
||||
setOpenModalFolder(false);
|
||||
setOpenModalInstallation(false);
|
||||
};
|
||||
|
||||
const handleInstallationFormSubmit = () => {
|
||||
setOpenModalFolder(false);
|
||||
setOpenModalInstallation(false);
|
||||
};
|
||||
|
||||
const handleFormCancel = () => {
|
||||
setOpenModalFolder(false);
|
||||
setOpenModalInstallation(false);
|
||||
};
|
||||
const areRequiredFieldsFilled = () => {
|
||||
for (const field of requiredFields) {
|
||||
if (!formValues[field]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (folderId == props.current_folder.id) {
|
||||
return (
|
||||
<>
|
||||
{openModalDeleteFolder && (
|
||||
<Modal
|
||||
open={openModalDeleteFolder}
|
||||
onClose={() => setOpenModalDeleteFolder(false)}
|
||||
aria-labelledby="error-modal"
|
||||
aria-describedby="error-modal-description"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 350,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Do you want to delete this folder?
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
gutterBottom
|
||||
sx={{ fontSize: '0.875rem' }}
|
||||
>
|
||||
All installations of this folder will be deleted.
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
onClick={deleteFolderModalHandle}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
marginLeft: 2,
|
||||
textTransform: 'none',
|
||||
bgcolor: '#ffc04d',
|
||||
color: '#111111',
|
||||
'&:hover': { bgcolor: '#f7b34d' }
|
||||
}}
|
||||
onClick={deleteFolderModalHandleCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</Modal>
|
||||
)}
|
||||
{openModalFolder && (
|
||||
<FolderForm
|
||||
cancel={handleFormCancel}
|
||||
submit={handleFolderFormSubmit}
|
||||
parentid={props.current_folder.id}
|
||||
/>
|
||||
)}
|
||||
{openModalInstallation && (
|
||||
<InstallationForm
|
||||
cancel={handleFormCancel}
|
||||
submit={handleInstallationFormSubmit}
|
||||
parentid={props.current_folder.id}
|
||||
/>
|
||||
)}
|
||||
<Grid item xs={12} md={12}>
|
||||
<TabsContainerWrapper>
|
||||
<Tabs
|
||||
|
@ -268,7 +69,18 @@ function Folder(props: singleFolderProps) {
|
|||
indicatorColor="primary"
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<Tab key={tab.value} label={tab.label} value={tab.value} />
|
||||
<Tab
|
||||
key={tab.value}
|
||||
label={tab.label}
|
||||
value={tab.value}
|
||||
component={Link}
|
||||
to={
|
||||
location.pathname.substring(
|
||||
0,
|
||||
location.pathname.lastIndexOf('/') + 1
|
||||
) + routes[tab.value]
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</TabsContainerWrapper>
|
||||
|
@ -280,202 +92,34 @@ function Folder(props: singleFolderProps) {
|
|||
alignItems="stretch"
|
||||
spacing={0}
|
||||
>
|
||||
{currentTab === 'folder' && (
|
||||
<Container maxWidth="xl">
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
alignItems="stretch"
|
||||
spacing={3}
|
||||
>
|
||||
<Grid item xs={12} md={12}>
|
||||
<CardContent>
|
||||
<Box
|
||||
component="form"
|
||||
sx={{
|
||||
'& .MuiTextField-root': { m: 1, width: '50ch' }
|
||||
}}
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="name"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
<Routes>
|
||||
<Route
|
||||
path={routes.information}
|
||||
element={
|
||||
<TreeInformation
|
||||
folder={props.current_folder}
|
||||
></TreeInformation>
|
||||
}
|
||||
name="name"
|
||||
value={formValues.name}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
required
|
||||
error={formValues.name === ''}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="information"
|
||||
defaultMessage="Information"
|
||||
/>
|
||||
}
|
||||
name="information"
|
||||
value={formValues.information}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
{currentUser.hasWriteAccess && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleDeleteFolder}
|
||||
sx={{
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="deleteFolder"
|
||||
defaultMessage="Delete Folder"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentUser.hasWriteAccess && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleNewFolderInsertion}
|
||||
sx={{
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="addNewFolder"
|
||||
defaultMessage="Add new folder"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentUser.hasWriteAccess && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleNewInstallationInsertion}
|
||||
sx={{
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="addNewInstallation"
|
||||
defaultMessage="Add new installation"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentUser.hasWriteAccess && (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleFolderInformationUpdate}
|
||||
sx={{
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
disabled={!areRequiredFieldsFilled()}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="applyChanges"
|
||||
defaultMessage="Apply Changes"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
marginLeft: '20px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{
|
||||
ml: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="errorOccured"
|
||||
defaultMessage="An error has occurred"
|
||||
/>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setError(false)} // Set error state to false on click
|
||||
sx={{ marginLeft: '4px' }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
{updated && (
|
||||
<Alert
|
||||
severity="success"
|
||||
sx={{
|
||||
ml: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="successfullyUpdated"
|
||||
defaultMessage="Successfully updated"
|
||||
/>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setUpdated(false)} // Set error state to false on click
|
||||
sx={{ marginLeft: '4px' }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
)}
|
||||
{currentTab === 'manage' && currentUser.hasWriteAccess && (
|
||||
<Route
|
||||
path={routes.manage}
|
||||
element={
|
||||
<AccessContextProvider>
|
||||
<Access
|
||||
currentResource={formValues}
|
||||
resourceType="folder"
|
||||
></Access>
|
||||
</AccessContextProvider>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Routes>
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default Folder;
|
||||
|
|
|
@ -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;
|
|
@ -5,8 +5,10 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import CustomTreeItem from './CustomTreeItem';
|
||||
import Installation from '../Installations/Installation';
|
||||
import Folder from './Folder';
|
||||
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import routes from '../../../Resources/routes.json';
|
||||
import Folder from './Folder';
|
||||
|
||||
function InstallationTree() {
|
||||
const { foldersAndInstallations, fetchAllFoldersAndInstallations } =
|
||||
|
@ -65,24 +67,38 @@ function InstallationTree() {
|
|||
</TreeView>
|
||||
</Grid>
|
||||
|
||||
<Routes>
|
||||
{foldersAndInstallations.map((installation) => {
|
||||
if (installation.type == 'Installation') {
|
||||
return (
|
||||
<Route
|
||||
key={installation.id}
|
||||
path={routes.installation + installation.id + '*'}
|
||||
element={
|
||||
<Installation
|
||||
key={installation.id + installation.type}
|
||||
key={installation.id}
|
||||
current_installation={installation}
|
||||
type="tree"
|
||||
type="installation"
|
||||
></Installation>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Route
|
||||
key={installation.id}
|
||||
path={routes.folder + installation.id + '*'}
|
||||
element={
|
||||
<Folder
|
||||
key={installation.id + installation.type}
|
||||
current_folder={installation}
|
||||
></Folder>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Routes>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
} from '@mui/material';
|
||||
import { InnovEnergyUser } from 'src/interfaces/UserTypes';
|
||||
import User from './User';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface FlatUsersViewProps {
|
||||
users: InnovEnergyUser[];
|
||||
|
@ -23,15 +22,10 @@ interface FlatUsersViewProps {
|
|||
|
||||
const FlatUsersView = (props: FlatUsersViewProps) => {
|
||||
const [selectedUser, setSelectedUser] = useState<number>(-1);
|
||||
const selectedBulkActions = selectedUser !== -1;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSelectOneUser = (installationID: number): void => {
|
||||
if (selectedUser != installationID) {
|
||||
setSelectedUser(installationID);
|
||||
// navigate(routes.users + '?user=' + installationID.toString(), {
|
||||
// replace: true
|
||||
// });
|
||||
const handleSelectOneUser = (userID: number): void => {
|
||||
if (selectedUser != userID) {
|
||||
setSelectedUser(userID);
|
||||
} else {
|
||||
setSelectedUser(-1);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,13 @@ import { FetchResult } from '../dataCache/dataCache';
|
|||
import { I_S3Credentials } from './S3Types';
|
||||
import { UnixTime } from '../dataCache/time';
|
||||
|
||||
export interface chartInfoInterface {
|
||||
magnitude: number;
|
||||
unit: string;
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface overviewInterface {
|
||||
soc: chartInfoInterface;
|
||||
temperature: chartInfoInterface;
|
||||
|
@ -18,13 +25,6 @@ export interface overviewInterface {
|
|||
overview: chartInfoInterface;
|
||||
}
|
||||
|
||||
export interface chartInfoInterface {
|
||||
magnitude: number;
|
||||
unit: string;
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface chartAggregatedDataInterface {
|
||||
minsoc: { name: string; data: number[] };
|
||||
maxsoc: { name: string; data: number[] };
|
||||
|
@ -45,6 +45,197 @@ export interface chartDataInterface {
|
|||
dcBusVoltage: { name: string; data: number[] };
|
||||
}
|
||||
|
||||
export interface BatteryDataInterface {
|
||||
Soc: { name: string; data: [] };
|
||||
Temperature: { name: string; data: [] };
|
||||
Power: { name: string; data: [] };
|
||||
Voltage: { name: string; data: [] };
|
||||
Current: { name: string; data: [] };
|
||||
}
|
||||
|
||||
export interface BatteryOverviewInterface {
|
||||
Soc: chartInfoInterface;
|
||||
Temperature: chartInfoInterface;
|
||||
Power: chartInfoInterface;
|
||||
Voltage: chartInfoInterface;
|
||||
Current: chartInfoInterface;
|
||||
}
|
||||
|
||||
export const transformInputToBatteryViewData = async (
|
||||
s3Credentials: I_S3Credentials,
|
||||
startTimestamp: UnixTime,
|
||||
endTimestamp: UnixTime
|
||||
): Promise<{
|
||||
chartData: BatteryDataInterface;
|
||||
chartOverview: BatteryOverviewInterface;
|
||||
}> => {
|
||||
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
||||
const MAX_NUMBER = 9999999;
|
||||
|
||||
const categories = ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
|
||||
const pathCategories = [
|
||||
'Soc',
|
||||
'Temperatures/Cells/Center',
|
||||
'Dc/Power',
|
||||
'Dc/Voltage',
|
||||
'Dc/Current'
|
||||
];
|
||||
|
||||
const pathsToSearch = [
|
||||
'/Battery/Devices/1/',
|
||||
'/Battery/Devices/2/',
|
||||
'/Battery/Devices/3/',
|
||||
'/Battery/Devices/4/',
|
||||
'/Battery/Devices/5/',
|
||||
'/Battery/Devices/6/',
|
||||
'/Battery/Devices/7/',
|
||||
'/Battery/Devices/8/',
|
||||
'/Battery/Devices/9/',
|
||||
'/Battery/Devices/10/'
|
||||
];
|
||||
|
||||
const pathsToSave = [
|
||||
'Battery1',
|
||||
'Battery2',
|
||||
'Battery3',
|
||||
'Battery4',
|
||||
'Battery5',
|
||||
'Battery6',
|
||||
'Battery7',
|
||||
'Battery8',
|
||||
'Battery9',
|
||||
'Battery10'
|
||||
];
|
||||
|
||||
const chartData: BatteryDataInterface = {
|
||||
Soc: { name: 'State Of Charge', data: [] },
|
||||
Temperature: { name: 'Temperature', data: [] },
|
||||
Power: { name: 'Power', data: [] },
|
||||
Voltage: { name: 'Voltage', data: [] },
|
||||
Current: { name: 'Voltage', data: [] }
|
||||
};
|
||||
|
||||
const chartOverview: BatteryOverviewInterface = {
|
||||
Soc: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
Temperature: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
Power: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
Voltage: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||
Current: { magnitude: 0, unit: '', min: 0, max: 0 }
|
||||
};
|
||||
|
||||
categories.forEach((category) => {
|
||||
chartData[category].data = [];
|
||||
pathsToSave.forEach((path) => {
|
||||
chartData[category].data[path] = { name: path, data: [] };
|
||||
});
|
||||
|
||||
chartOverview[category] = {
|
||||
magnitude: 0,
|
||||
unit: '',
|
||||
min: MAX_NUMBER,
|
||||
max: -MAX_NUMBER
|
||||
};
|
||||
});
|
||||
|
||||
let adjustedTimestampArray = [];
|
||||
|
||||
let startTimestampToNum = Number(startTimestamp);
|
||||
if (startTimestampToNum % 2 != 0) {
|
||||
startTimestampToNum += 1;
|
||||
}
|
||||
let startUnixTime = UnixTime.fromTicks(startTimestampToNum);
|
||||
let diff = endTimestamp.ticks - startUnixTime.ticks;
|
||||
|
||||
const timestampPromises = [];
|
||||
|
||||
while (startUnixTime < endTimestamp) {
|
||||
timestampPromises.push(fetchData(startUnixTime, s3Credentials));
|
||||
|
||||
startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + diff / 100);
|
||||
if (startUnixTime.ticks % 2 !== 0) {
|
||||
startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + 1);
|
||||
}
|
||||
const adjustedTimestamp = new Date(startUnixTime.ticks * 1000);
|
||||
adjustedTimestamp.setHours(adjustedTimestamp.getHours() + 1);
|
||||
adjustedTimestampArray.push(adjustedTimestamp);
|
||||
}
|
||||
|
||||
const results = await Promise.all(timestampPromises);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
if (
|
||||
result === FetchResult.notAvailable ||
|
||||
result === FetchResult.tryLater
|
||||
) {
|
||||
// Handle not available or try later case
|
||||
} else {
|
||||
for (
|
||||
let category_index = 0;
|
||||
category_index < pathCategories.length;
|
||||
category_index++
|
||||
) {
|
||||
let category = categories[category_index];
|
||||
|
||||
for (let j = 0; j < pathsToSearch.length; j++) {
|
||||
let path = pathsToSearch[j] + pathCategories[category_index];
|
||||
|
||||
if (result[path]) {
|
||||
const value = result[path];
|
||||
|
||||
if (value.value < chartOverview[category].min) {
|
||||
chartOverview[category].min = value.value;
|
||||
}
|
||||
|
||||
if (value.value > chartOverview[category].max) {
|
||||
chartOverview[category].max = value.value;
|
||||
}
|
||||
chartData[category].data[pathsToSave[j]].data.push([
|
||||
adjustedTimestampArray[i],
|
||||
value.value
|
||||
]);
|
||||
} else {
|
||||
//data[path].push([adjustedTimestamp, null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
categories.forEach((category) => {
|
||||
let value = Math.max(
|
||||
Math.abs(chartOverview[category].max),
|
||||
Math.abs(chartOverview[category].min)
|
||||
);
|
||||
let magnitude = 0;
|
||||
|
||||
if (value < 0) {
|
||||
value = -value;
|
||||
}
|
||||
while (value >= 1000) {
|
||||
value /= 1000;
|
||||
magnitude++;
|
||||
}
|
||||
chartOverview[category].magnitude = magnitude;
|
||||
});
|
||||
|
||||
chartOverview.Soc.unit = '(%)';
|
||||
chartOverview.Soc.min = 0;
|
||||
chartOverview.Soc.max = 100;
|
||||
chartOverview.Temperature.unit = '(°C)';
|
||||
chartOverview.Power.unit =
|
||||
'(' + prefixes[chartOverview['Power'].magnitude] + 'W' + ')';
|
||||
chartOverview.Voltage.unit =
|
||||
'(' + prefixes[chartOverview['Voltage'].magnitude] + 'V' + ')';
|
||||
|
||||
chartOverview.Current.unit =
|
||||
'(' + prefixes[chartOverview['Current'].magnitude] + 'A' + ')';
|
||||
|
||||
return {
|
||||
chartData: chartData,
|
||||
chartOverview: chartOverview
|
||||
};
|
||||
};
|
||||
|
||||
export const transformInputToDailyData = async (
|
||||
s3Credentials: I_S3Credentials,
|
||||
startTimestamp: UnixTime,
|
||||
|
@ -53,8 +244,6 @@ export const transformInputToDailyData = async (
|
|||
chartData: chartDataInterface;
|
||||
chartOverview: overviewInterface;
|
||||
}> => {
|
||||
const data = {};
|
||||
const overviewData = {};
|
||||
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
||||
const MAX_NUMBER = 9999999;
|
||||
const pathsToSearch = [
|
||||
|
@ -65,6 +254,14 @@ export const transformInputToDailyData = async (
|
|||
'/PvOnDc/Dc/Power',
|
||||
'/DcDc/Dc/Link/Voltage'
|
||||
];
|
||||
const categories = [
|
||||
'soc',
|
||||
'temperature',
|
||||
'dcPower',
|
||||
'gridPower',
|
||||
'pvProduction',
|
||||
'dcBusVoltage'
|
||||
];
|
||||
|
||||
const chartData: chartDataInterface = {
|
||||
soc: { name: 'State Of Charge', data: [] },
|
||||
|
@ -86,9 +283,9 @@ export const transformInputToDailyData = async (
|
|||
overview: { magnitude: 0, unit: '', min: 0, max: 0 }
|
||||
};
|
||||
|
||||
pathsToSearch.forEach((path) => {
|
||||
data[path] = [];
|
||||
overviewData[path] = {
|
||||
categories.forEach((category) => {
|
||||
chartData[category].data = [];
|
||||
chartOverview[category] = {
|
||||
magnitude: 0,
|
||||
unit: '',
|
||||
min: MAX_NUMBER,
|
||||
|
@ -130,29 +327,33 @@ export const transformInputToDailyData = async (
|
|||
// Handle not available or try later case
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
let category_index = 0;
|
||||
pathsToSearch.forEach((path) => {
|
||||
if (result[path]) {
|
||||
const value = result[path];
|
||||
|
||||
if (value.value < overviewData[path].min) {
|
||||
overviewData[path].min = value.value;
|
||||
if (value.value < chartOverview[categories[category_index]].min) {
|
||||
chartOverview[categories[category_index]].min = value.value;
|
||||
}
|
||||
|
||||
if (value.value > overviewData[path].max) {
|
||||
overviewData[path].max = value.value;
|
||||
if (value.value > chartOverview[categories[category_index]].max) {
|
||||
chartOverview[categories[category_index]].max = value.value;
|
||||
}
|
||||
|
||||
data[path].push([adjustedTimestampArray[i], value.value]);
|
||||
chartData[categories[category_index]].data.push([
|
||||
adjustedTimestampArray[i],
|
||||
value.value
|
||||
]);
|
||||
} else {
|
||||
//data[path].push([adjustedTimestamp, null]);
|
||||
}
|
||||
category_index++;
|
||||
});
|
||||
}
|
||||
}
|
||||
pathsToSearch.forEach((path) => {
|
||||
categories.forEach((category) => {
|
||||
let value = Math.max(
|
||||
Math.abs(overviewData[path].max),
|
||||
Math.abs(overviewData[path].min)
|
||||
Math.abs(chartOverview[category].max),
|
||||
Math.abs(chartOverview[category].min)
|
||||
);
|
||||
let magnitude = 0;
|
||||
|
||||
|
@ -163,81 +364,35 @@ export const transformInputToDailyData = async (
|
|||
value /= 1000;
|
||||
magnitude++;
|
||||
}
|
||||
overviewData[path].magnitude = magnitude;
|
||||
chartOverview[category].magnitude = magnitude;
|
||||
});
|
||||
|
||||
let path = '/Battery/Soc';
|
||||
chartData.soc.data = data[path];
|
||||
|
||||
chartOverview.soc = {
|
||||
unit: '(%)',
|
||||
magnitude: overviewData[path].magnitude,
|
||||
min: 0,
|
||||
max: 100
|
||||
};
|
||||
|
||||
path = '/Battery/Temperature';
|
||||
chartData.temperature.data = data[path];
|
||||
|
||||
chartOverview.temperature = {
|
||||
unit: '(°C)',
|
||||
magnitude: overviewData[path].magnitude,
|
||||
min: overviewData[path].min,
|
||||
max: overviewData[path].max
|
||||
};
|
||||
|
||||
path = '/Battery/Dc/Power';
|
||||
chartData.dcPower.data = data[path];
|
||||
|
||||
chartOverview.dcPower = {
|
||||
magnitude: overviewData[path].magnitude,
|
||||
unit: '(' + prefixes[overviewData[path].magnitude] + 'W' + ')',
|
||||
min: overviewData[path].min,
|
||||
max: overviewData[path].max
|
||||
};
|
||||
|
||||
path = '/GridMeter/Ac/Power/Active';
|
||||
chartData.gridPower.data = data[path];
|
||||
|
||||
chartOverview.gridPower = {
|
||||
magnitude: overviewData[path].magnitude,
|
||||
unit: '(' + prefixes[overviewData[path].magnitude] + 'W' + ')',
|
||||
min: overviewData[path].min,
|
||||
max: overviewData[path].max
|
||||
};
|
||||
|
||||
path = '/PvOnDc/Dc/Power';
|
||||
chartData.pvProduction.data = data[path];
|
||||
|
||||
chartOverview.pvProduction = {
|
||||
magnitude: overviewData[path].magnitude,
|
||||
unit: '(' + prefixes[overviewData[path].magnitude] + 'W' + ')',
|
||||
min: overviewData[path].min,
|
||||
max: overviewData[path].max
|
||||
};
|
||||
|
||||
path = '/DcDc/Dc/Link/Voltage';
|
||||
chartData.dcBusVoltage.data = data[path];
|
||||
chartOverview.dcBusVoltage = {
|
||||
magnitude: overviewData[path].magnitude,
|
||||
unit: '(' + prefixes[overviewData[path].magnitude] + 'V' + ')',
|
||||
min: overviewData[path].min,
|
||||
max: overviewData[path].max
|
||||
};
|
||||
chartOverview.soc.unit = '(%)';
|
||||
chartOverview.soc.min = 0;
|
||||
chartOverview.soc.max = 100;
|
||||
chartOverview.temperature.unit = '(°C)';
|
||||
chartOverview.dcPower.unit =
|
||||
'(' + prefixes[chartOverview['dcPower'].magnitude] + 'W' + ')';
|
||||
chartOverview.gridPower.unit =
|
||||
'(' + prefixes[chartOverview['gridPower'].magnitude] + 'W' + ')';
|
||||
chartOverview.pvProduction.unit =
|
||||
'(' + prefixes[chartOverview['pvProduction'].magnitude] + 'W' + ')';
|
||||
chartOverview.dcBusVoltage.unit =
|
||||
'(' + prefixes[chartOverview['dcBusVoltage'].magnitude] + 'V' + ')';
|
||||
|
||||
chartOverview.overview = {
|
||||
magnitude: Math.max(
|
||||
overviewData['/GridMeter/Ac/Power/Active'].magnitude,
|
||||
overviewData['/PvOnDc/Dc/Power'].magnitude
|
||||
chartOverview['gridPower'].magnitude,
|
||||
chartOverview['pvProduction'].magnitude
|
||||
),
|
||||
unit: '(kW)',
|
||||
min: Math.min(
|
||||
overviewData['/GridMeter/Ac/Power/Active'].min,
|
||||
overviewData['/PvOnDc/Dc/Power'].min
|
||||
chartOverview['gridPower'].min,
|
||||
chartOverview['pvProduction'].min
|
||||
),
|
||||
max: Math.max(
|
||||
overviewData['/GridMeter/Ac/Power/Active'].max,
|
||||
overviewData['/PvOnDc/Dc/Power'].max
|
||||
chartOverview['gridPower'].max,
|
||||
chartOverview['pvProduction'].max
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -277,6 +432,17 @@ export const transformInputToAggregatedData = async (
|
|||
'/HeatingPower'
|
||||
];
|
||||
|
||||
const categories = [
|
||||
'minsoc',
|
||||
'maxsoc',
|
||||
'pvProduction',
|
||||
'dcChargingPower',
|
||||
'heatingPower',
|
||||
'dcDischargingPower',
|
||||
'gridImportPower',
|
||||
'gridExportPower'
|
||||
];
|
||||
|
||||
const chartAggregatedData: chartAggregatedDataInterface = {
|
||||
minsoc: { name: 'min SOC', data: [] },
|
||||
maxsoc: { name: 'max SOC', data: [] },
|
||||
|
|
|
@ -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/"
|
||||
}
|
Loading…
Reference in New Issue