diff --git a/csharp/App/SaliMax/src/Program.cs b/csharp/App/SaliMax/src/Program.cs index 24926ae3a..32d796e10 100644 --- a/csharp/App/SaliMax/src/Program.cs +++ b/csharp/App/SaliMax/src/Program.cs @@ -299,6 +299,7 @@ internal static class Program { subscribedNow = true; _subscribeToQueueForTheFirstTime = true; + _prevSalimaxState = currentSalimaxState.Status; _subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp); } diff --git a/csharp/Lib/Devices/Battery48TL/Battery48TlRecord.Api.cs b/csharp/Lib/Devices/Battery48TL/Battery48TlRecord.Api.cs index 43fd695ea..3cfa41f9f 100644 --- a/csharp/Lib/Devices/Battery48TL/Battery48TlRecord.Api.cs +++ b/csharp/Lib/Devices/Battery48TL/Battery48TlRecord.Api.cs @@ -128,8 +128,8 @@ public partial class Battery48TlRecord { Boolean HasBit(Int16 bit) => (_AlarmFlags & 1uL << bit) > 0; - if (HasBit(0) ) yield return "Tam : BMS temperature too low"; - if (HasBit(2) ) yield return "TaM2 : BMS temperature too high"; + if (HasBit(0)) yield return "Tam : BMS temperature too low"; + if (HasBit(2)) yield return "TaM2 : BMS temperature too high"; if (HasBit(3) ) yield return "Tbm : Battery temperature too low"; if (HasBit(5) ) yield return "TbM2 : Battery temperature too high"; if (HasBit(7) ) yield return "VBm2 : Bus voltage too low"; diff --git a/typescript/frontend-marios2/src/App.tsx b/typescript/frontend-marios2/src/App.tsx index ba1934e1b..77abadedb 100644 --- a/typescript/frontend-marios2/src/App.tsx +++ b/typescript/frontend-marios2/src/App.tsx @@ -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) => ( }> @@ -58,11 +49,6 @@ function App() { ); - // 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() { } /> - } /> (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,155 +71,225 @@ 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 ( - - - - - Battery - Firmware - Power - Voltage - SoC - Temperature - Warnings - Alarms - - - - {batteryData.map((battery) => ( - - - {battery.BatteryId} - - + + + + + } + /> + + {currentTab === 'batteryview' && ( + + - backgroundColor: - battery.Voltage < 44 || battery.Voltage > 57 - ? '#FF033E' - : '#32CD32', - color: battery.Voltage === '' ? 'white' : 'inherit' - }} - > - {battery.Voltage} - - - {battery.Soc} - - 270 ? '#FF033E' : '#32CD32 ' - }} - > - {battery.AverageTemperature} - + + + + )} + - - {battery.Warnings === '' ? ( - 'None' - ) : battery.Warnings.split(';').length > 1 ? ( - +
+ + + Battery + Firmware + Power + Voltage + SoC + Temperature + Warnings + Alarms + + + + {batteryData.map((battery) => ( + - Multiple Warnings - - ) : ( - battery.Warnings - )} - - - {battery.Alarms === '' ? ( - 'None' - ) : battery.Alarms.split(';').length > 1 ? ( - - Multiple Alarms - - ) : ( - battery.Alarms - )} - - - ))} - -
-
+ + {battery.BatteryId} + + + {battery.FwVersion} + + + {battery.Power} + + 57 + ? '#FF033E' + : '#32CD32', + color: battery.Voltage === '' ? 'white' : 'inherit' + }} + > + {battery.Voltage} + + + {battery.Soc} + + 270 + ? '#FF033E' + : '#32CD32 ' + }} + > + {battery.AverageTemperature} + + + + {battery.Warnings === '' ? ( + 'None' + ) : battery.Warnings.split(';').length > 1 ? ( + + Multiple Warnings + + ) : ( + battery.Warnings + )} + + + {battery.Alarms === '' ? ( + 'None' + ) : battery.Alarms.split(';').length > 1 ? ( + + Multiple Alarms + + ) : ( + battery.Alarms + )} + + + ))} + + + + )} + + ); } diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx new file mode 100644 index 000000000..5d3352a44 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx @@ -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 && ( + + + + Fetching data... + + + )} + {isErrorDateModalOpen && ( + {}}> + + + {dateSelectionError} + + + + + + )} + {isDateModalOpen && ( + + {}}> + + setStartDate(newDate)} + sx={{ + marginTop: 2 + }} + /> + + setEndDate(newDate)} + sx={{ + marginTop: 2 + }} + /> + +
+ + + +
+
+
+
+ )} + + {!loading && ( + <> + {' '} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + ); +} + +export default MainStats; diff --git a/typescript/frontend-marios2/src/content/dashboards/Information/Information.tsx b/typescript/frontend-marios2/src/content/dashboards/Information/Information.tsx new file mode 100644 index 000000000..e733045a7 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Information/Information.tsx @@ -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 && ( + + + + Do you want to delete this installation? + + +
+ + +
+
+
+ )} + + + + + +
+ + } + name="name" + value={formValues.name} + onChange={handleChange} + fullWidth + required + error={formValues.name === ''} + /> +
+
+ + } + name="region" + value={formValues.region} + onChange={handleChange} + variant="outlined" + fullWidth + required + error={formValues.region === ''} + /> +
+
+ + } + name="location" + value={formValues.location} + onChange={handleChange} + variant="outlined" + fullWidth + required + error={formValues.location === ''} + /> +
+
+ + } + name="country" + value={formValues.country} + onChange={handleChange} + variant="outlined" + fullWidth + required + error={formValues.country === ''} + /> +
+
+ + } + name="orderNumbers" + value={formValues.orderNumbers} + onChange={handleChange} + variant="outlined" + fullWidth + /> +
+
+ + } + name="installationName" + value={formValues.installationName} + onChange={handleChange} + variant="outlined" + fullWidth + /> +
+ + {currentUser.hasWriteAccess && ( + <> +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + )} + +
+ {currentUser.hasWriteAccess && ( + + )} + {currentUser.hasWriteAccess && ( + + )} + + {loading && ( + + )} + {error && ( + + + setError(false)} + sx={{ marginLeft: '4px' }} + > + + + + )} + {updated && ( + + + + setUpdated(false)} // Set error state to false on click + sx={{ marginLeft: '4px' }} + > + + + + )} +
+
+
+
+
+
+ + ); +} + +export default Information; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx index 58445c1ab..aa5d789c3 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx @@ -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(-1); + const currentLocation = useLocation(); const handleSelectOneInstallation = (installationID: number): void => { if (selectedInstallation != installationID) { setSelectedInstallation(installationID); - navigate(`?installation=${installationID}`, { - replace: true - }); + 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 ( - + @@ -225,14 +235,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { - - {props.installations.map((installation) => ( - - ))} ); }; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx index f1a333266..8c0295fdc 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx @@ -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(undefined); const [values, setValues] = useState(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 == 'configuration' || - currentTab == 'batteryview') + currentTab == 'live' || + currentTab == 'configuration' || + 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,145 +111,47 @@ 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 && ( - + +
+ - - - Do you want to delete this installation? - - -
- - -
-
- - )} - - -
- - - - - {props.current_installation.name} - -
- {currentTab == 'live' && values && ( -
- - - - - {values.mode.values[0].value} - -
- )} + +
+ + {props.current_installation.name} + +
+ {currentTab == 'live' && values && (
- Status: + -
- {status === -1 ? ( - - ) : ( - '' - )} - - {status === -2 ? ( - - ) : ( - '' - )} - -
+
+ )} +
+ + Status: + +
+ {status === -1 ? ( + -
+ ) : ( + '' + )} + + {status === -2 ? ( + + ) : ( + '' + )} + +
+
- - - {currentTab === 'information' && ( - - - - - -
- - } - name="name" - value={formValues.name} - onChange={handleChange} - fullWidth - required - error={formValues.name === ''} - /> -
-
- - } - name="region" - value={formValues.region} - onChange={handleChange} - variant="outlined" - fullWidth - required - error={formValues.name === ''} - /> -
-
- - } - name="location" - value={formValues.location} - onChange={handleChange} - variant="outlined" - fullWidth - required - error={formValues.name === ''} - /> -
-
- - } - name="country" - value={formValues.country} - onChange={handleChange} - variant="outlined" - fullWidth - required - error={formValues.name === ''} - /> -
-
- - } - name="orderNumbers" - value={formValues.orderNumbers} - onChange={handleChange} - variant="outlined" - fullWidth - /> -
-
- - } - name="installationName" - value={formValues.installationName} - onChange={handleChange} - variant="outlined" - fullWidth - /> -
+ + + + + } + /> - {currentUser.hasWriteAccess && ( - <> -
- -
+ + } + /> -
- -
+ } + /> -
- -
+ } + /> -
- -
- - )} + + } + /> -
- {currentUser.hasWriteAccess && ( - - )} - {currentUser.hasWriteAccess && ( - - )} - - {loading && ( - - )} - {error && ( - - - setError(false)} - sx={{ marginLeft: '4px' }} - > - - - - )} - {updated && ( - - - - setUpdated(false)} // Set error state to false on click - sx={{ marginLeft: '4px' }} - > - - - - )} -
-
-
-
-
-
+ {currentUser.hasWriteAccess && ( + + } + /> )} - {currentTab === 'overview' && ( - + {currentUser.hasWriteAccess && ( + + + + } + /> )} - {currentTab === 'batteryview' && ( - - )} - {currentTab === 'configuration' && currentUser.hasWriteAccess && ( - - )} - {currentTab === 'manage' && currentUser.hasWriteAccess && ( - - - - )} - {currentTab === 'live' && } - {currentTab === 'log' && ( - - )} -
-
- - - ); - } else { - return null; - } + + } + /> + + + + + + ); } export default Installation; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/InstallationSearch.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/InstallationSearch.tsx index c6cda3cca..28f12d3b4 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/InstallationSearch.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/InstallationSearch.tsx @@ -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' + }} > + + {filteredData.map((installation) => { + return ( + + } + /> + ); + })} + ); } diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx index 376a2d256..94bd65089 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx @@ -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(searchParams.get('tab')); + const { currentUser } = context; + const tabList = [ + 'live', + 'overview', + 'manage', + 'batteryview', + 'log', + 'information', + 'configuration' + ]; + + const [currentTab, setCurrentTab] = useState(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,64 +133,115 @@ function InstallationTabs() { } ]; - const tabs = installationId - ? currentUser.hasWriteAccess - ? [ - { - value: 'list', - icon: - }, - { - value: 'tree', - icon: - }, - { - value: 'live', - label: - }, - { - value: 'overview', - label: - }, - { - value: 'batteryview', - label: ( - - ) - }, - { - value: 'manage', - label: ( - - ) - }, - { - value: 'log', - label: - }, - { - value: 'information', - label: ( - - ) - }, + const tabs = + currentTab != 'list' && + currentTab != 'tree' && + !location.pathname.includes('folder') + ? currentUser.hasWriteAccess + ? [ + { + value: 'list', + icon: + }, + { + value: 'tree', + icon: + }, + { + value: 'live', + label: + }, + { + value: 'overview', + label: ( + + ) + }, + { + value: 'batteryview', + label: ( + + ) + }, + { + value: 'manage', + label: ( + + ) + }, + { + value: 'log', + label: + }, + { + value: 'information', + label: ( + + ) + }, - { - value: 'configuration', - label: ( - - ) - } - ] + { + value: 'configuration', + label: ( + + ) + } + ] + : [ + { + value: 'list', + icon: + }, + { + value: 'tree', + icon: + }, + + { + value: 'live', + label: + }, + { + value: 'overview', + label: ( + + ) + }, + { + value: 'batteryview', + label: ( + + ) + }, + { + value: 'log', + label: + }, + { + value: 'information', + label: ( + + ) + } + ] : [ { value: 'list', @@ -230,46 +250,8 @@ function InstallationTabs() { { value: 'tree', icon: - }, - - { - value: 'live', - label: - }, - { - value: 'overview', - label: - }, - { - value: 'batteryview', - label: ( - - ) - }, - { - value: 'log', - label: - }, - { - value: 'information', - label: ( - - ) } - ] - : [ - { - value: 'list', - icon: - }, - { - value: 'tree', - icon: - } - ]; + ]; return installations.length > 1 ? ( <> @@ -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() { } /> } /> + + } + > @@ -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] + } /> ))} diff --git a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx index 2b3eb7013..6c7c4dddb 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx @@ -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; -}; diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx index 006ce83af..b51e29278 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx @@ -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' }, diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx index 723924cda..35fa89765 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -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) { > - + {/**/} + {/* */} + {/**/} {dailyData && ( <>
- - {/**/} { 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) {
} 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', diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/Folder.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/Folder.tsx index e40cf0754..199335bf2 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Tree/Folder.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Tree/Folder.tsx @@ -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('folder'); + const [currentTab, setCurrentTab] = useState(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(-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: + value: 'information', + label: }, { value: 'manage', @@ -88,394 +56,70 @@ 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 && ( - setOpenModalDeleteFolder(false)} - aria-labelledby="error-modal" - aria-describedby="error-modal-description" + return ( + <> + + + - - - Do you want to delete this folder? - - - All installations of this folder will be deleted. - - -
- - -
-
-
- )} - {openModalFolder && ( - - )} - {openModalInstallation && ( - - )} - - - - {tabs.map((tab) => ( - - ))} - - - - - {currentTab === 'folder' && ( - - - - - -
- - } - name="name" - value={formValues.name} - onChange={handleChange} - fullWidth - required - error={formValues.name === ''} - /> -
-
- - } - name="information" - value={formValues.information} - onChange={handleChange} - variant="outlined" - fullWidth - /> -
- -
- {currentUser.hasWriteAccess && ( - - )} - - {currentUser.hasWriteAccess && ( - - )} - - {currentUser.hasWriteAccess && ( - - )} - - {currentUser.hasWriteAccess && ( - - )} - - {loading && ( - - )} - {error && ( - - - setError(false)} // Set error state to false on click - sx={{ marginLeft: '4px' }} - > - - - - )} - {updated && ( - - - setUpdated(false)} // Set error state to false on click - sx={{ marginLeft: '4px' }} - > - - - - )} -
-
-
-
-
-
+ {tabs.map((tab) => ( + + ))} + + + + + + + } + /> + {currentUser.hasWriteAccess && ( + + + + } + /> )} - {currentTab === 'manage' && currentUser.hasWriteAccess && ( - - - - )} - - -
- - ); - } else { - return null; - } + +
+ +
+ + ); } export default Folder; diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx new file mode 100644 index 000000000..272e9626e --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx @@ -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 && ( + setOpenModalDeleteFolder(false)} + aria-labelledby="error-modal" + aria-describedby="error-modal-description" + > + + + Do you want to delete this folder? + + + All installations of this folder will be deleted. + + +
+ + +
+
+
+ )} + {openModalFolder && ( + + )} + {openModalInstallation && ( + + )} + + + + + +
+ } + name="name" + value={formValues.name} + onChange={handleChange} + fullWidth + required + error={formValues.name === ''} + /> +
+
+ + } + name="information" + value={formValues.information} + onChange={handleChange} + variant="outlined" + fullWidth + /> +
+ +
+ {currentUser.hasWriteAccess && ( + + )} + + {currentUser.hasWriteAccess && ( + + )} + + {currentUser.hasWriteAccess && ( + + )} + + {currentUser.hasWriteAccess && ( + + )} + + {loading && ( + + )} + {error && ( + + + setError(false)} // Set error state to false on click + sx={{ marginLeft: '4px' }} + > + + + + )} + {updated && ( + + + setUpdated(false)} // Set error state to false on click + sx={{ marginLeft: '4px' }} + > + + + + )} +
+
+
+
+
+
+ + ); +} + +export default TreeInformation; diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/InstallationTree.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/InstallationTree.tsx index 1ae3b371c..650f5f842 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Tree/InstallationTree.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Tree/InstallationTree.tsx @@ -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() { - {foldersAndInstallations.map((installation) => { - if (installation.type == 'Installation') { - return ( - - ); - } else { - return ( - - ); - } - })} + + {foldersAndInstallations.map((installation) => { + if (installation.type == 'Installation') { + return ( + + } + /> + ); + } else { + return ( + + } + /> + ); + } + })} + ); } diff --git a/typescript/frontend-marios2/src/content/dashboards/Users/FlatUsersView.tsx b/typescript/frontend-marios2/src/content/dashboards/Users/FlatUsersView.tsx index 86a8502f7..7e3d8abd6 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Users/FlatUsersView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Users/FlatUsersView.tsx @@ -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(-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); } diff --git a/typescript/frontend-marios2/src/interfaces/Chart.tsx b/typescript/frontend-marios2/src/interfaces/Chart.tsx index 1c209eb69..804c0d475 100644 --- a/typescript/frontend-marios2/src/interfaces/Chart.tsx +++ b/typescript/frontend-marios2/src/interfaces/Chart.tsx @@ -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: [] }, diff --git a/typescript/frontend-marios2/src/routes.json b/typescript/frontend-marios2/src/routes.json deleted file mode 100644 index 9cfa4f8f7..000000000 --- a/typescript/frontend-marios2/src/routes.json +++ /dev/null @@ -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/" -}