Added history tab

This commit is contained in:
Noe 2024-06-11 21:35:35 +02:00
parent b55cc076cf
commit e6c32b7162
7 changed files with 354 additions and 163 deletions

View File

@ -29,7 +29,6 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import axiosConfig from '../../../Resources/axiosConfig'; import axiosConfig from '../../../Resources/axiosConfig';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
import { Action } from '../../../interfaces/S3Types';
import { UserContext } from '../../../contexts/userContext'; import { UserContext } from '../../../contexts/userContext';
interface ConfigurationProps { interface ConfigurationProps {
@ -136,14 +135,6 @@ function Configuration(props: ConfigurationProps) {
.add(localOffset, 'minute') .add(localOffset, 'minute')
.toDate() .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)); // console.log('will send ', dayjs(formValues.calibrationChargeDate));
setLoading(true); setLoading(true);
@ -160,23 +151,10 @@ function Configuration(props: ConfigurationProps) {
}); });
if (res) { if (res) {
const historyRes = await axiosConfig
.post(
`/UpdateActionHistory?installationId=${props.id}`,
historyAction
)
.catch((err) => {
if (err.response) {
setError(true);
setLoading(false);
}
});
if (historyRes) {
setUpdated(true); setUpdated(true);
setLoading(false); setLoading(false);
} }
} }
}
}; };
const handleOkOnErrorDateModal = () => { const handleOkOnErrorDateModal = () => {

View File

@ -122,30 +122,42 @@ function HistoryOfActions(props: HistoryProps) {
<FormattedMessage id="time" defaultMessage="Time" /> <FormattedMessage id="time" defaultMessage="Time" />
</Typography> </Typography>
</div> </div>
{/*<div*/} <div
{/* style={{*/} style={{
{/* flex: 1,*/} flex: 1,
{/* marginTop: '15px',*/} marginTop: '15px',
{/* display: 'flex',*/} display: 'flex',
{/* alignItems: 'center',*/} alignItems: 'center',
{/* justifyContent: 'center'*/} justifyContent: 'center'
{/* }}*/} }}
{/*>*/} >
{/* <Typography*/} <Typography
{/* variant="body1"*/} variant="body1"
{/* color="dimgrey"*/} color="dimgrey"
{/* fontWeight="bold"*/} fontWeight="bold"
{/* fontSize="1rem"*/} fontSize="1rem"
{/* gutterBottom*/} gutterBottom
{/* noWrap*/} noWrap
{/* >*/} >
{/* <FormattedMessage id="seen" defaultMessage="Seen" />*/} <FormattedMessage
{/* </Typography>*/} id="description"
{/*</div>*/} defaultMessage="Description"
/>
</Typography>
</div>
</div> </div>
<Divider /> <Divider />
<div style={{ maxHeight: '400px', overflowY: 'auto' }}> <div style={{ maxHeight: '400px', overflowY: 'auto' }}>
{history.map((action, index) => ( {history.map((action, index) => {
// Parse the timestamp string to a Date object
const date = new Date(action.timestamp);
// Extract the date part (e.g., "2023-05-31")
const datePart = date.toLocaleDateString();
// Extract the time part (e.g., "12:34:56")
const timePart = date.toLocaleTimeString();
return (
<> <>
<Divider /> <Divider />
<div <div
@ -173,7 +185,7 @@ function HistoryOfActions(props: HistoryProps) {
gutterBottom gutterBottom
noWrap noWrap
> >
{action.user} {action.userName}
</Typography> </Typography>
</div> </div>
@ -193,7 +205,7 @@ function HistoryOfActions(props: HistoryProps) {
gutterBottom gutterBottom
noWrap noWrap
> >
{action.date} {datePart}
</Typography> </Typography>
</div> </div>
<div <div
@ -212,12 +224,33 @@ function HistoryOfActions(props: HistoryProps) {
gutterBottom gutterBottom
noWrap noWrap
> >
{action.time} {timePart}
</Typography>
</div>
<div
style={{
flex: 3,
display: 'flex',
marginTop: '15px',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
>
{action.description}
</Typography> </Typography>
</div> </div>
</div> </div>
</> </>
))} );
})}
</div> </div>
</div> </div>
</Card> </Card>

View File

@ -24,6 +24,7 @@ import Information from '../Information/Information';
import BatteryView from '../BatteryView/BatteryView'; import BatteryView from '../BatteryView/BatteryView';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import HistoryOfActions from '../History/History'; import HistoryOfActions from '../History/History';
import PvView from '../PvView/PvView';
interface singleInstallationProps { interface singleInstallationProps {
current_installation?: I_Installation; current_installation?: I_Installation;
@ -124,6 +125,7 @@ function Installation(props: singleInstallationProps) {
useEffect(() => { useEffect(() => {
if ( if (
currentTab == 'live' || currentTab == 'live' ||
currentTab == 'pvview' ||
currentTab == 'configuration' || currentTab == 'configuration' ||
location.includes('batteryview') location.includes('batteryview')
) { ) {
@ -131,7 +133,8 @@ function Installation(props: singleInstallationProps) {
if ( if (
currentTab == 'live' || currentTab == 'live' ||
(location.includes('batteryview') && !location.includes('mainstats')) (location.includes('batteryview') && !location.includes('mainstats')) ||
currentTab == 'pvview'
) { ) {
fetchDataPeriodically(); fetchDataPeriodically();
interval = setInterval(fetchDataPeriodically, 2000); interval = setInterval(fetchDataPeriodically, 2000);
@ -144,6 +147,7 @@ function Installation(props: singleInstallationProps) {
return () => { return () => {
if ( if (
currentTab == 'live' || currentTab == 'live' ||
currentTab == 'pvview' ||
(location.includes('batteryview') && !location.includes('mainstats')) (location.includes('batteryview') && !location.includes('mainstats'))
) { ) {
clearInterval(interval); clearInterval(interval);
@ -314,13 +318,7 @@ function Installation(props: singleInstallationProps) {
<Route <Route
path={routes.pvview + '*'} path={routes.pvview + '*'}
element={ element={
<PvView <PvView values={values} connected={connected}></PvView>
values={values}
s3Credentials={s3Credentials}
installationId={props.current_installation.id}
productNum={props.current_installation.product}
connected={connected}
></PvView>
} }
></Route> ></Route>

View File

@ -140,15 +140,15 @@ function InstallationTabs() {
/> />
) )
}, },
// { {
// value: 'history', value: 'history',
// label: ( label: (
// <FormattedMessage <FormattedMessage
// id="history" id="history"
// defaultMessage="History Of Actions" defaultMessage="History Of Actions"
// /> />
// ) )
// }, },
{ {
value: 'pvview', value: 'pvview',
label: <FormattedMessage id="pvview" defaultMessage="Pv View" /> label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
@ -271,16 +271,16 @@ function InstallationTabs() {
defaultMessage="Configuration" defaultMessage="Configuration"
/> />
) )
},
{
value: 'history',
label: (
<FormattedMessage
id="history"
defaultMessage="History Of Actions"
/>
)
} }
// {
// value: 'history',
// label: (
// <FormattedMessage
// id="history"
// defaultMessage="History Of Actions"
// />
// )
// }
] ]
: currentUser.userType == UserType.partner : currentUser.userType == UserType.partner
? [ ? [

View File

@ -37,8 +37,8 @@ export type ConfigurationValues = {
export interface Pv { export interface Pv {
PvId: number; PvId: number;
Voltage: I_BoxDataValue;
Power: I_BoxDataValue; Power: I_BoxDataValue;
Voltage: I_BoxDataValue;
Current: I_BoxDataValue; Current: I_BoxDataValue;
} }
@ -86,6 +86,8 @@ export interface Battery {
MaxDischargePower: I_BoxDataValue; MaxDischargePower: I_BoxDataValue;
} }
const PvKeys = ['PvId', 'Power', 'Voltage', 'Current'];
const BatteryKeys = [ const BatteryKeys = [
'BatteryId', 'BatteryId',
'FwVersion', 'FwVersion',
@ -177,10 +179,15 @@ type TopologyPaths = { [key in keyof TopologyValues]: string[] };
const batteryIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const batteryIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const pvIds = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30
];
const PvPaths = [ const PvPaths = [
'/PvOnDc/Strings/%id%/Power',
'/PvOnDc/Strings/%id%/Voltage', '/PvOnDc/Strings/%id%/Voltage',
'/PvOnDc/Strings/%id%/Current', '/PvOnDc/Strings/%id%/Current'
'/PvOnDc/Strings/%id%/Power'
]; ];
const batteryPaths = [ const batteryPaths = [
@ -305,7 +312,7 @@ export const topologyPaths: TopologyPaths = {
batteryPaths.map((path) => path.replace('%id%', id.toString())) batteryPaths.map((path) => path.replace('%id%', id.toString()))
), ),
pvView: batteryIds.flatMap((id) => pvView: pvIds.flatMap((id) =>
PvPaths.map((path) => path.replace('%id%', id.toString())) PvPaths.map((path) => path.replace('%id%', id.toString()))
), ),
@ -336,9 +343,6 @@ export const extractValues = (
timeSeriesData: DataPoint timeSeriesData: DataPoint
): TopologyValues | null => { ): TopologyValues | null => {
const extractedValues: TopologyValues = {} as TopologyValues; const extractedValues: TopologyValues = {} as TopologyValues;
// console.log('timeSeriesData=', timeSeriesData);
for (const topologyKey of Object.keys(topologyPaths)) { for (const topologyKey of Object.keys(topologyPaths)) {
//Each topologykey may have more than one paths (for example inverter) //Each topologykey may have more than one paths (for example inverter)
const paths = topologyPaths[topologyKey]; const paths = topologyPaths[topologyKey];
@ -346,12 +350,6 @@ export const extractValues = (
if (topologyKey === 'pvView') { if (topologyKey === 'pvView') {
extractedValues[topologyKey] = []; extractedValues[topologyKey] = [];
const node_ids_from_csv = timeSeriesData.value[
'/Config/Devices/BatteryNodes'
].value
.toString()
.split(',');
let pv_index = 0; let pv_index = 0;
let pathIndex = 0; let pathIndex = 0;
@ -359,16 +357,16 @@ export const extractValues = (
let pv = {}; let pv = {};
let existingKeys = 0; let existingKeys = 0;
//We prepare a battery object for each node. We extract the nodes from the '/Config/Devices/BatteryNodes' path. For example, nodes can be [2,4,5,6] (one is missing) //We prepare a pv object for each node. We extract the number of nodes from the '/PvOnDc/NbrOfStrings' path.
//BatteryKeys[0] is the battery id. We set the battery id to the corresponding node id. //PvKeys[0] is the pv id.
battery[BatteryKeys[0]] = node_ids_from_csv[pv_index]; pv[PvKeys[0]] = pv_index;
//Then, search all the remaining battery keys //Then, search all the remaining battery keys
for (let i = 1; i < BatteryKeys.length; i++) { for (let i = 1; i < PvKeys.length; i++) {
const path = paths[pathIndex]; const path = paths[pathIndex];
if (timeSeriesData.value.hasOwnProperty(path)) { if (timeSeriesData.value.hasOwnProperty(path)) {
existingKeys++; existingKeys++;
battery[BatteryKeys[i]] = { pv[PvKeys[i]] = {
unit: timeSeriesData.value[path].unit.includes('~') unit: timeSeriesData.value[path].unit.includes('~')
? timeSeriesData.value[path].unit.replace('~', '') ? timeSeriesData.value[path].unit.replace('~', '')
: timeSeriesData.value[path].unit, : timeSeriesData.value[path].unit,
@ -380,14 +378,12 @@ export const extractValues = (
} }
pathIndex++; pathIndex++;
} }
battery_index++; pv_index++;
if (existingKeys > 0) { if (existingKeys > 0) {
extractedValues[topologyKey].push(battery as Battery); extractedValues[topologyKey].push(pv as Pv);
} }
} }
} } else if (topologyKey === 'batteryView') {
if (topologyKey === 'batteryView') {
extractedValues[topologyKey] = []; extractedValues[topologyKey] = [];
const node_ids_from_csv = timeSeriesData.value[ const node_ids_from_csv = timeSeriesData.value[
'/Config/Devices/BatteryNodes' '/Config/Devices/BatteryNodes'

View File

@ -0,0 +1,187 @@
import React, { useEffect, useState } from 'react';
import {
Container,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography
} from '@mui/material';
import { TopologyValues } from '../Log/graph.util';
import { useLocation, useNavigate } from 'react-router-dom';
import routes from '../../../Resources/routes.json';
import CircularProgress from '@mui/material/CircularProgress';
interface PvViewProps {
values: TopologyValues;
connected: boolean;
}
function PvView(props: PvViewProps) {
if (props.values === null && props.connected == true) {
return null;
}
const currentLocation = useLocation();
const navigate = useNavigate();
const sortedPvView =
props.values != null
? [...props.values.pvView].sort((a, b) => a.PvId - b.PvId)
: [];
const [loading, setLoading] = useState(sortedPvView.length == 0);
const handleMainStatsButton = () => {
navigate(routes.mainstats);
};
const findBatteryData = (batteryId: number) => {
for (let i = 0; i < props.values.batteryView.length; i++) {
if (props.values.batteryView[i].BatteryId == batteryId) {
return props.values.batteryView[i];
}
}
};
useEffect(() => {
if (sortedPvView.length == 0) {
setLoading(true);
} else {
setLoading(false);
}
}, [sortedPvView]);
return (
<>
{!props.connected && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '70vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography
variant="body2"
style={{ color: 'black', fontWeight: 'bold' }}
mt={2}
>
Unable to communicate with the installation
</Typography>
<Typography variant="body2" style={{ color: 'black' }}>
Please wait or refresh the page
</Typography>
</Container>
)}
{loading && props.connected && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '70vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography
variant="body2"
style={{ color: 'black', fontWeight: 'bold' }}
mt={2}
>
Battery service is not available at the moment
</Typography>
<Typography variant="body2" style={{ color: 'black' }}>
Please wait or refresh the page
</Typography>
</Container>
)}
{!loading && props.connected && (
<Container maxWidth="xl">
<TableContainer
component={Paper}
sx={{
marginTop: '20px',
marginBottom: '20px'
}}
>
<Table sx={{ minWidth: 250 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="center">Pv</TableCell>
<TableCell align="center">Power</TableCell>
<TableCell align="center">Voltage</TableCell>
<TableCell align="center">Current</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sortedPvView.map((pv) => (
<TableRow
key={pv.PvId}
style={{
height: '10px'
}}
>
<TableCell
component="th"
scope="row"
align="center"
sx={{ width: '10%', fontWeight: 'bold', color: 'black' }}
>
{'String ' + pv.PvId}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
pv.Current.value == 0 ? '#FF033E' : '#32CD32',
color: pv.Power.value === '' ? 'white' : 'inherit'
}}
>
{pv.Power.value + ' ' + pv.Power.unit}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
pv.Current.value == 0 ? '#FF033E' : '#32CD32',
color: pv.Voltage.value === '' ? 'white' : 'inherit'
}}
>
{pv.Voltage.value + ' ' + pv.Voltage.unit}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
pv.Current.value == 0 ? '#FF033E' : '#32CD32',
color: pv.Current.value === '' ? 'white' : 'inherit'
}}
>
{pv.Current.value + ' ' + pv.Current.unit}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Container>
)}
</>
);
}
export default PvView;

View File

@ -1,5 +1,3 @@
import { ConfigurationValues } from '../content/dashboards/Log/graph.util';
export interface I_S3Credentials { export interface I_S3Credentials {
s3Region: string; s3Region: string;
s3Provider: string; s3Provider: string;
@ -20,8 +18,9 @@ export interface ErrorMessage {
} }
export interface Action { export interface Action {
configuration: ConfigurationValues; id: number;
date: string; userName: string;
time: string; installationId: number;
user: string; timestamp: string;
description: String;
} }