diff --git a/csharp/App/SaliMax/src/Program.cs b/csharp/App/SaliMax/src/Program.cs index ee0a76355..e54ddb987 100644 --- a/csharp/App/SaliMax/src/Program.cs +++ b/csharp/App/SaliMax/src/Program.cs @@ -678,35 +678,36 @@ internal static class Program // This is temporary for Wittman, but now it's for all Instalattion await File.WriteAllTextAsync("/var/www/html/status.csv", csv.SplitLines().Where(l => !l.Contains("Secret")).JoinLines()); - + + var response = await request.PutAsync(new StringContent(csv)); // Compress CSV data to a byte array - byte[] compressedBytes; - using (var memoryStream = new MemoryStream()) - { - //Create a zip directory and put the compressed file inside - using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) - { - var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive - using (var entryStream = entry.Open()) - using (var writer = new StreamWriter(entryStream)) - { - writer.Write(csv); - } - } - - compressedBytes = memoryStream.ToArray(); - } - - // Encode the compressed byte array as a Base64 string - string base64String = Convert.ToBase64String(compressedBytes); - - // Create StringContent from Base64 string - var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64"); - - // Upload the compressed data (ZIP archive) to S3 - var response = await request.PutAsync(stringContent); - + // byte[] compressedBytes; + // using (var memoryStream = new MemoryStream()) + // { + // //Create a zip directory and put the compressed file inside + // using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) + // { + // var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive + // using (var entryStream = entry.Open()) + // using (var writer = new StreamWriter(entryStream)) + // { + // writer.Write(csv); + // } + // } + // + // compressedBytes = memoryStream.ToArray(); + // } + // + // // Encode the compressed byte array as a Base64 string + // string base64String = Convert.ToBase64String(compressedBytes); + // + // // Create StringContent from Base64 string + // var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64"); + // + // // Upload the compressed data (ZIP archive) to S3 + // var response = await request.PutAsync(stringContent); + // if (response.StatusCode != 200) { Console.WriteLine("ERROR: PUT"); diff --git a/csharp/App/SaliMax/src/S3Config.cs b/csharp/App/SaliMax/src/S3Config.cs index 741a5c565..cbf2a92f9 100644 --- a/csharp/App/SaliMax/src/S3Config.cs +++ b/csharp/App/SaliMax/src/S3Config.cs @@ -70,7 +70,8 @@ public record S3Config // CanonicalizedResource; - contentType = "application/base64; charset=utf-8"; + //contentType = "application/base64; charset=utf-8"; + contentType = "text/plain; charset=utf-8"; var payload = $"{method}\n{md5Hash}\n{contentType}\n{date}\n/{bucket.Trim('/')}/{s3Path.Trim('/')}"; using var hmacSha1 = new HMACSHA1(UTF8.GetBytes(s3Secret)); diff --git a/typescript/frontend-marios2/src/Resources/routes.json b/typescript/frontend-marios2/src/Resources/routes.json index f86097f4b..538990ff5 100644 --- a/typescript/frontend-marios2/src/Resources/routes.json +++ b/typescript/frontend-marios2/src/Resources/routes.json @@ -15,6 +15,7 @@ "live": "live", "information": "information", "configuration": "configuration", + "history": "history", "mainstats": "mainstats", "detailed_view": "detailed_view/" } diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx index 7861a57c8..63ca5b839 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx @@ -246,8 +246,8 @@ function MainStats(props: MainStatsProps) { chartOverview: BatteryOverviewInterface; }> = transformInputToBatteryViewData( props.s3Credentials, - UnixTime.fromTicks(startX), - UnixTime.fromTicks(endX) + UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)), + UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2)) ); resultPromise diff --git a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx index 66b682adf..449d12308 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx @@ -15,7 +15,7 @@ import { Typography, useTheme } from '@mui/material'; -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import Button from '@mui/material/Button'; import { Close as CloseIcon } from '@mui/icons-material'; @@ -29,6 +29,8 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import dayjs from 'dayjs'; import axiosConfig from '../../../Resources/axiosConfig'; import utc from 'dayjs/plugin/utc'; +import { Action } from '../../../interfaces/S3Types'; +import { UserContext } from '../../../contexts/userContext'; interface ConfigurationProps { values: TopologyValues; @@ -82,6 +84,8 @@ function Configuration(props: ConfigurationProps) { const [updated, setUpdated] = useState(false); const [dateSelectionError, setDateSelectionError] = useState(''); const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false); + const context = useContext(UserContext); + const { currentUser, setUser } = context; const [formValues, setFormValues] = useState({ minimumSoC: props.values.minimumSoC[0].value, @@ -133,6 +137,13 @@ function Configuration(props: ConfigurationProps) { .toDate() }; + const historyAction: Action = { + configuration: configurationToSend, + date: new Date().toISOString().split('T')[0], // Gets the current date in YYYY-MM-DD format + time: new Date().toISOString().split('T')[1].split('.')[0], // Gets the current time in HH:MM:SS format + user: currentUser.name + }; + // console.log('will send ', dayjs(formValues.calibrationChargeDate)); setLoading(true); @@ -149,8 +160,21 @@ function Configuration(props: ConfigurationProps) { }); if (res) { - setUpdated(true); - setLoading(false); + const historyRes = await axiosConfig + .post( + `/UpdateActionHistory?installationId=${props.id}`, + historyAction + ) + .catch((err) => { + if (err.response) { + setError(true); + setLoading(false); + } + }); + if (historyRes) { + setUpdated(true); + setLoading(false); + } } } }; diff --git a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx new file mode 100644 index 000000000..49ffd8a68 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx @@ -0,0 +1,275 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { + Alert, + Card, + Container, + Divider, + Grid, + IconButton, + useTheme +} from '@mui/material'; +import Typography from '@mui/material/Typography'; +import { FormattedMessage } from 'react-intl'; +import axiosConfig from '../../../Resources/axiosConfig'; +import { AxiosError, AxiosResponse } from 'axios/index'; +import routes from '../../../Resources/routes.json'; +import { useNavigate } from 'react-router-dom'; +import { TokenContext } from '../../../contexts/tokenContext'; +import { Action } from '../../../interfaces/S3Types'; + +interface HistoryProps { + errorLoadingS3Data: boolean; + id: number; +} + +function HistoryOfActions(props: HistoryProps) { + const theme = useTheme(); + const searchParams = new URLSearchParams(location.search); + + const [history, setHistory] = useState([]); + const navigate = useNavigate(); + const tokencontext = useContext(TokenContext); + const { removeToken } = tokencontext; + + useEffect(() => { + axiosConfig + .get(`/GetHistoryForInstallation?id=${props.id}`) + .then((res: AxiosResponse) => { + setHistory(res.data); + }) + .catch((err: AxiosError) => { + if (err.response && err.response.status == 401) { + removeToken(); + navigate(routes.login); + } + }); + }, []); + + return ( + + + + {history.length > 0 && ( + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + +
+ {/**/} + {/* */} + {/* */} + {/* */} + {/*
*/} +
+ +
+ {history.map((action, index) => ( + <> + +
+
+ + {action.user} + +
+ +
+ + {action.date} + +
+
+ + {action.time} + +
+
+ + ))} +
+ +
+ )} + + {!props.errorLoadingS3Data && history.length == 0 && ( + + + + + )} +
+
+ + + {props.errorLoadingS3Data && ( + + + + + )} + +
+ ); +} + +export default HistoryOfActions; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx index e812e91fa..909250ad5 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx @@ -23,6 +23,7 @@ import routes from '../../../Resources/routes.json'; import Information from '../Information/Information'; import BatteryView from '../BatteryView/BatteryView'; import { UserType } from '../../../interfaces/UserTypes'; +import HistoryOfActions from '../History/History'; interface singleInstallationProps { current_installation?: I_Installation; @@ -342,6 +343,18 @@ function Installation(props: singleInstallationProps) { } /> )} + {currentUser.userType == UserType.admin && ( + + } + /> + )} + {currentUser.userType == UserType.admin && ( (undefined); @@ -137,6 +138,15 @@ function InstallationTabs() { defaultMessage="Configuration" /> ) + }, + { + value: 'history', + label: ( + + ) } ] : currentUser.userType == UserType.partner @@ -248,6 +258,15 @@ function InstallationTabs() { defaultMessage="Configuration" /> ) + }, + { + value: 'history', + label: ( + + ) } ] : currentUser.userType == UserType.partner diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx index 8881c7f1e..1bd5daa80 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -119,8 +119,8 @@ function Overview(props: OverviewProps) { chartOverview: overviewInterface; }> = transformInputToDailyData( props.s3Credentials, - UnixTime.fromTicks(startX), - UnixTime.fromTicks(endX) + UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)), + UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2)) ); let isComponentMounted = true; diff --git a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx index b673f0d31..6e11a2a10 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx @@ -58,6 +58,28 @@ function Installation(props: singleInstallationProps) { const s3Credentials = { s3Bucket, ...S3data }; + const fetchDataOnlyOneTime = async () => { + var timeperiodToSearch = 70; + + for (var i = timeperiodToSearch; i > 0; i -= 2) { + const now = UnixTime.now().earlier(TimeSpan.fromSeconds(i)); + + const res = await fetchData(now, s3Credentials); + + if (res != FetchResult.notAvailable && res != FetchResult.tryLater) { + setConnected(true); + setFailedToCommunicateWithInstallation(0); + setValues( + extractValues({ + time: now, + value: res + }) + ); + return now; + } + } + }; + const fetchDataPeriodically = async () => { const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20)); @@ -76,7 +98,7 @@ function Installation(props: singleInstallationProps) { return true; } else { setFailedToCommunicateWithInstallation((prevCount) => { - if (prevCount + 1 >= 3) { + if (prevCount + 1 >= 20) { setConnected(false); } return prevCount + 1; @@ -87,19 +109,6 @@ function Installation(props: singleInstallationProps) { } }; - const fetchDataOnlyOneTime = async () => { - let success = false; - const max_retransmissions = 3; - - for (let i = 0; i < max_retransmissions; i++) { - success = await fetchDataPeriodically(); - await new Promise((resolve) => setTimeout(resolve, 1000)); - if (success) { - break; - } - } - }; - useEffect(() => { let path = location.split('/'); @@ -118,7 +127,7 @@ function Installation(props: singleInstallationProps) { currentTab == 'live' || (location.includes('batteryview') && !location.includes('mainstats')) ) { - fetchDataPeriodically(); + fetchDataOnlyOneTime(); interval = setInterval(fetchDataPeriodically, 2000); } if (currentTab == 'configuration' || location.includes('mainstats')) { diff --git a/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx b/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx index 17a73e4b2..e8c4b8260 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx @@ -222,6 +222,7 @@ function Topology(props: TopologyProps) { }} bottomBox={{ title: 'AC Loads', + data: props.values.islandBusToLoadOnIslandBusConnection, connected: props.values.loadOnIslandBusBox[0].value.toString() != diff --git a/typescript/frontend-marios2/src/interfaces/S3Types.tsx b/typescript/frontend-marios2/src/interfaces/S3Types.tsx index 7309b1cb5..078a011bc 100644 --- a/typescript/frontend-marios2/src/interfaces/S3Types.tsx +++ b/typescript/frontend-marios2/src/interfaces/S3Types.tsx @@ -1,3 +1,5 @@ +import { ConfigurationValues } from '../content/dashboards/Log/graph.util'; + export interface I_S3Credentials { s3Region: string; s3Provider: string; @@ -16,3 +18,10 @@ export interface ErrorMessage { deviceCreatedTheMessage: string; seen: boolean; } + +export interface Action { + configuration: ConfigurationValues; + date: string; + time: string; + user: string; +}