Added history tab
This commit is contained in:
parent
b55cc076cf
commit
e6c32b7162
|
@ -29,7 +29,6 @@ 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 {
|
||||
|
@ -136,14 +135,6 @@ function Configuration(props: ConfigurationProps) {
|
|||
.add(localOffset, 'minute')
|
||||
.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);
|
||||
|
@ -160,21 +151,8 @@ function Configuration(props: ConfigurationProps) {
|
|||
});
|
||||
|
||||
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);
|
||||
setLoading(false);
|
||||
}
|
||||
setUpdated(true);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -122,102 +122,135 @@ function HistoryOfActions(props: HistoryProps) {
|
|||
<FormattedMessage id="time" defaultMessage="Time" />
|
||||
</Typography>
|
||||
</div>
|
||||
{/*<div*/}
|
||||
{/* style={{*/}
|
||||
{/* flex: 1,*/}
|
||||
{/* marginTop: '15px',*/}
|
||||
{/* display: 'flex',*/}
|
||||
{/* alignItems: 'center',*/}
|
||||
{/* justifyContent: 'center'*/}
|
||||
{/* }}*/}
|
||||
{/*>*/}
|
||||
{/* <Typography*/}
|
||||
{/* variant="body1"*/}
|
||||
{/* color="dimgrey"*/}
|
||||
{/* fontWeight="bold"*/}
|
||||
{/* fontSize="1rem"*/}
|
||||
{/* gutterBottom*/}
|
||||
{/* noWrap*/}
|
||||
{/* >*/}
|
||||
{/* <FormattedMessage id="seen" defaultMessage="Seen" />*/}
|
||||
{/* </Typography>*/}
|
||||
{/*</div>*/}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="dimgrey"
|
||||
fontWeight="bold"
|
||||
fontSize="1rem"
|
||||
gutterBottom
|
||||
noWrap
|
||||
>
|
||||
<FormattedMessage
|
||||
id="description"
|
||||
defaultMessage="Description"
|
||||
/>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
||||
{history.map((action, index) => (
|
||||
<>
|
||||
<Divider />
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
height: '40px',
|
||||
marginBottom: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight="bold"
|
||||
color="text.primary"
|
||||
gutterBottom
|
||||
noWrap
|
||||
>
|
||||
{action.user}
|
||||
</Typography>
|
||||
</div>
|
||||
{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 />
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
height: '40px',
|
||||
marginBottom: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight="bold"
|
||||
color="text.primary"
|
||||
gutterBottom
|
||||
noWrap
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{action.date}
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight="bold"
|
||||
color="text.primary"
|
||||
gutterBottom
|
||||
noWrap
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight="bold"
|
||||
color="text.primary"
|
||||
gutterBottom
|
||||
noWrap
|
||||
>
|
||||
{action.userName}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{action.time}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight="bold"
|
||||
color="text.primary"
|
||||
gutterBottom
|
||||
noWrap
|
||||
>
|
||||
{datePart}
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight="bold"
|
||||
color="text.primary"
|
||||
gutterBottom
|
||||
noWrap
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
@ -24,6 +24,7 @@ import Information from '../Information/Information';
|
|||
import BatteryView from '../BatteryView/BatteryView';
|
||||
import { UserType } from '../../../interfaces/UserTypes';
|
||||
import HistoryOfActions from '../History/History';
|
||||
import PvView from '../PvView/PvView';
|
||||
|
||||
interface singleInstallationProps {
|
||||
current_installation?: I_Installation;
|
||||
|
@ -124,6 +125,7 @@ function Installation(props: singleInstallationProps) {
|
|||
useEffect(() => {
|
||||
if (
|
||||
currentTab == 'live' ||
|
||||
currentTab == 'pvview' ||
|
||||
currentTab == 'configuration' ||
|
||||
location.includes('batteryview')
|
||||
) {
|
||||
|
@ -131,7 +133,8 @@ function Installation(props: singleInstallationProps) {
|
|||
|
||||
if (
|
||||
currentTab == 'live' ||
|
||||
(location.includes('batteryview') && !location.includes('mainstats'))
|
||||
(location.includes('batteryview') && !location.includes('mainstats')) ||
|
||||
currentTab == 'pvview'
|
||||
) {
|
||||
fetchDataPeriodically();
|
||||
interval = setInterval(fetchDataPeriodically, 2000);
|
||||
|
@ -144,6 +147,7 @@ function Installation(props: singleInstallationProps) {
|
|||
return () => {
|
||||
if (
|
||||
currentTab == 'live' ||
|
||||
currentTab == 'pvview' ||
|
||||
(location.includes('batteryview') && !location.includes('mainstats'))
|
||||
) {
|
||||
clearInterval(interval);
|
||||
|
@ -314,13 +318,7 @@ function Installation(props: singleInstallationProps) {
|
|||
<Route
|
||||
path={routes.pvview + '*'}
|
||||
element={
|
||||
<PvView
|
||||
values={values}
|
||||
s3Credentials={s3Credentials}
|
||||
installationId={props.current_installation.id}
|
||||
productNum={props.current_installation.product}
|
||||
connected={connected}
|
||||
></PvView>
|
||||
<PvView values={values} connected={connected}></PvView>
|
||||
}
|
||||
></Route>
|
||||
|
||||
|
|
|
@ -140,15 +140,15 @@ function InstallationTabs() {
|
|||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'history',
|
||||
// label: (
|
||||
// <FormattedMessage
|
||||
// id="history"
|
||||
// defaultMessage="History Of Actions"
|
||||
// />
|
||||
// )
|
||||
// },
|
||||
{
|
||||
value: 'history',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="history"
|
||||
defaultMessage="History Of Actions"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'pvview',
|
||||
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
|
||||
|
@ -271,16 +271,16 @@ function InstallationTabs() {
|
|||
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
|
||||
? [
|
||||
|
|
|
@ -37,8 +37,8 @@ export type ConfigurationValues = {
|
|||
|
||||
export interface Pv {
|
||||
PvId: number;
|
||||
Voltage: I_BoxDataValue;
|
||||
Power: I_BoxDataValue;
|
||||
Voltage: I_BoxDataValue;
|
||||
Current: I_BoxDataValue;
|
||||
}
|
||||
|
||||
|
@ -86,6 +86,8 @@ export interface Battery {
|
|||
MaxDischargePower: I_BoxDataValue;
|
||||
}
|
||||
|
||||
const PvKeys = ['PvId', 'Power', 'Voltage', 'Current'];
|
||||
|
||||
const BatteryKeys = [
|
||||
'BatteryId',
|
||||
'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 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 = [
|
||||
'/PvOnDc/Strings/%id%/Power',
|
||||
'/PvOnDc/Strings/%id%/Voltage',
|
||||
'/PvOnDc/Strings/%id%/Current',
|
||||
'/PvOnDc/Strings/%id%/Power'
|
||||
'/PvOnDc/Strings/%id%/Current'
|
||||
];
|
||||
|
||||
const batteryPaths = [
|
||||
|
@ -305,7 +312,7 @@ export const topologyPaths: TopologyPaths = {
|
|||
batteryPaths.map((path) => path.replace('%id%', id.toString()))
|
||||
),
|
||||
|
||||
pvView: batteryIds.flatMap((id) =>
|
||||
pvView: pvIds.flatMap((id) =>
|
||||
PvPaths.map((path) => path.replace('%id%', id.toString()))
|
||||
),
|
||||
|
||||
|
@ -336,9 +343,6 @@ export const extractValues = (
|
|||
timeSeriesData: DataPoint
|
||||
): TopologyValues | null => {
|
||||
const extractedValues: TopologyValues = {} as TopologyValues;
|
||||
|
||||
// console.log('timeSeriesData=', timeSeriesData);
|
||||
|
||||
for (const topologyKey of Object.keys(topologyPaths)) {
|
||||
//Each topologykey may have more than one paths (for example inverter)
|
||||
const paths = topologyPaths[topologyKey];
|
||||
|
@ -346,12 +350,6 @@ export const extractValues = (
|
|||
|
||||
if (topologyKey === 'pvView') {
|
||||
extractedValues[topologyKey] = [];
|
||||
const node_ids_from_csv = timeSeriesData.value[
|
||||
'/Config/Devices/BatteryNodes'
|
||||
].value
|
||||
.toString()
|
||||
.split(',');
|
||||
|
||||
let pv_index = 0;
|
||||
let pathIndex = 0;
|
||||
|
||||
|
@ -359,16 +357,16 @@ export const extractValues = (
|
|||
let pv = {};
|
||||
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)
|
||||
//BatteryKeys[0] is the battery id. We set the battery id to the corresponding node id.
|
||||
battery[BatteryKeys[0]] = node_ids_from_csv[pv_index];
|
||||
//We prepare a pv object for each node. We extract the number of nodes from the '/PvOnDc/NbrOfStrings' path.
|
||||
//PvKeys[0] is the pv id.
|
||||
pv[PvKeys[0]] = pv_index;
|
||||
//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];
|
||||
if (timeSeriesData.value.hasOwnProperty(path)) {
|
||||
existingKeys++;
|
||||
|
||||
battery[BatteryKeys[i]] = {
|
||||
pv[PvKeys[i]] = {
|
||||
unit: timeSeriesData.value[path].unit.includes('~')
|
||||
? timeSeriesData.value[path].unit.replace('~', '')
|
||||
: timeSeriesData.value[path].unit,
|
||||
|
@ -380,14 +378,12 @@ export const extractValues = (
|
|||
}
|
||||
pathIndex++;
|
||||
}
|
||||
battery_index++;
|
||||
pv_index++;
|
||||
if (existingKeys > 0) {
|
||||
extractedValues[topologyKey].push(battery as Battery);
|
||||
extractedValues[topologyKey].push(pv as Pv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (topologyKey === 'batteryView') {
|
||||
} else if (topologyKey === 'batteryView') {
|
||||
extractedValues[topologyKey] = [];
|
||||
const node_ids_from_csv = timeSeriesData.value[
|
||||
'/Config/Devices/BatteryNodes'
|
||||
|
|
|
@ -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;
|
|
@ -1,5 +1,3 @@
|
|||
import { ConfigurationValues } from '../content/dashboards/Log/graph.util';
|
||||
|
||||
export interface I_S3Credentials {
|
||||
s3Region: string;
|
||||
s3Provider: string;
|
||||
|
@ -20,8 +18,9 @@ export interface ErrorMessage {
|
|||
}
|
||||
|
||||
export interface Action {
|
||||
configuration: ConfigurationValues;
|
||||
date: string;
|
||||
time: string;
|
||||
user: string;
|
||||
id: number;
|
||||
userName: string;
|
||||
installationId: number;
|
||||
timestamp: string;
|
||||
description: String;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue