Merge remote-tracking branch 'origin/marios'

This commit is contained in:
Kim 2023-10-09 14:58:03 +02:00
commit ec1681311a
32 changed files with 2591 additions and 942 deletions

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 BloomUI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,9 @@
"@types/react-dom": "17.0.13", "@types/react-dom": "17.0.13",
"apexcharts": "3.35.3", "apexcharts": "3.35.3",
"axios": "^1.5.0", "axios": "^1.5.0",
"chart.js": "^4.4.0",
"clsx": "1.1.1", "clsx": "1.1.1",
"cytoscape": "^3.26.0",
"date-fns": "2.28.0", "date-fns": "2.28.0",
"history": "5.3.0", "history": "5.3.0",
"linq-to-typescript": "^11.0.0", "linq-to-typescript": "^11.0.0",
@ -23,9 +25,14 @@
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "17.0.2", "react": "17.0.2",
"react-apexcharts": "1.4.0", "react-apexcharts": "1.4.0",
"react-chartjs-2": "^5.2.0",
"react-custom-scrollbars-2": "4.4.0", "react-custom-scrollbars-2": "4.4.0",
"react-cytoscapejs": "^2.0.0",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-flow-renderer": "^10.3.17",
"react-helmet-async": "1.3.0", "react-helmet-async": "1.3.0",
"react-icons": "^4.11.0",
"react-icons-converter": "^1.1.4",
"react-intl": "^6.4.4", "react-intl": "^6.4.4",
"react-router": "6.3.0", "react-router": "6.3.0",
"react-router-dom": "6.3.0", "react-router-dom": "6.3.0",

View File

@ -15,13 +15,13 @@ import SidebarLayout from './layouts/SidebarLayout';
import { TokenContext } from './contexts/tokenContext'; import { TokenContext } from './contexts/tokenContext';
import ResetPassword from './components/ResetPassword'; import ResetPassword from './components/ResetPassword';
import ForgotPassword from './components/ForgotPassword'; import ForgotPassword from './components/ForgotPassword';
import InstallationTabs from './content/dashboards/Installations/index';
import routes from 'src/Resources/routes.json';
import './App.css';
function App() { function App() {
//const content = useRoutes(router);
const context = useContext(UserContext); const context = useContext(UserContext);
const { currentUser, setUser } = context; const { currentUser, setUser } = context;
const tokencontext = useContext(TokenContext); const tokencontext = useContext(TokenContext);
const { token, setNewToken, removeToken } = tokencontext; const { token, setNewToken, removeToken } = tokencontext;
const [forgotPassword, setForgotPassword] = useState(false); const [forgotPassword, setForgotPassword] = useState(false);
@ -79,14 +79,14 @@ function App() {
lazy(() => import('src/content/pages/Status/Maintenance')) lazy(() => import('src/content/pages/Status/Maintenance'))
); );
const routes: RouteObject[] = [ const routesArray: RouteObject[] = [
{ {
path: '', path: '',
element: <BaseLayout />, element: <BaseLayout />,
children: [ children: [
{ {
path: '/', path: '/',
element: <Navigate to="installations" replace /> element: <Navigate to="installations/" replace />
}, },
{ {
path: 'status', path: 'status',
@ -118,39 +118,6 @@ function App() {
element: <Status404 /> element: <Status404 />
} }
] ]
},
{
path: 'ResetPassword',
element: <ResetPassword></ResetPassword>
},
{
path: 'Login',
element: <Login></Login>
},
{
path: 'installations',
element: (
<SidebarLayout language={language} onSelectLanguage={setLanguage} />
),
children: [
{
path: '',
element: <Installations />
}
]
},
{
path: 'users',
element: (
<SidebarLayout language={language} onSelectLanguage={setLanguage} />
),
children: [
{
path: '',
element: <Users />
}
]
} }
]; ];
if (forgotPassword) { if (forgotPassword) {
@ -189,7 +156,7 @@ function App() {
> >
<CssBaseline /> <CssBaseline />
<Routes> <Routes>
{routes.map((route, index) => ( {routesArray.map((route, index) => (
<Route key={index} path={route.path} element={route.element}> <Route key={index} path={route.path} element={route.element}>
{route.children && {route.children &&
route.children.map((childRoute, childIndex) => ( route.children.map((childRoute, childIndex) => (
@ -201,6 +168,23 @@ function App() {
))} ))}
</Route> </Route>
))} ))}
<Route
path="/"
element={
<SidebarLayout
language={language}
onSelectLanguage={setLanguage}
/>
}
>
<Route
path={routes.installations + '*'}
element={<InstallationTabs />}
/>
<Route path={routes.users + '*'} element={<Users />} />
<Route path="ResetPassword" element={<ResetPassword />}></Route>
<Route path="Login" element={<Login />}></Route>
</Route>
</Routes> </Routes>
</IntlProvider> </IntlProvider>
</ThemeProvider> </ThemeProvider>

View File

@ -1,8 +1,7 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { import {
Card, Card,
CircularProgress, CircularProgress,
Divider,
Grid, Grid,
Table, Table,
TableBody, TableBody,
@ -18,26 +17,44 @@ import Installation from './Installation';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import { LogContext } from 'src/contexts/LogContextProvider'; import { LogContext } from 'src/contexts/LogContextProvider';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useNavigate } from 'react-router-dom';
import routes from 'src/Resources/routes.json';
interface FlatInstallationViewProps { interface FlatInstallationViewProps {
installations: I_Installation[]; installations: I_Installation[];
} }
const FlatInstallationView = (props: FlatInstallationViewProps) => { const FlatInstallationView = (props: FlatInstallationViewProps) => {
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
const selectedBulkActions = selectedInstallation === -1 ? false : true;
const [isRowHovered, setHoveredRow] = useState(-1); const [isRowHovered, setHoveredRow] = useState(-1);
const logContext = useContext(LogContext); const logContext = useContext(LogContext);
const { getStatus } = logContext; const { getStatus } = logContext;
const navigate = useNavigate();
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
const handleSelectOneInstallation = (installationID: number): void => { const handleSelectOneInstallation = (installationID: number): void => {
if (selectedInstallation != installationID) { if (selectedInstallation != installationID) {
setSelectedInstallation(installationID); setSelectedInstallation(installationID);
navigate(
routes.installations +
routes.list +
'?installation=' +
installationID.toString(),
{
replace: true
}
);
} else { } else {
setSelectedInstallation(-1); setSelectedInstallation(-1);
} }
}; };
useEffect(() => {
setSelectedInstallation(installationId);
}, [installationId]);
const theme = useTheme(); const theme = useTheme();
const findInstallation = (id: number) => { const findInstallation = (id: number) => {
@ -54,20 +71,27 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
return ( return (
<Grid container spacing={1} sx={{ marginTop: 0.1 }}> <Grid container spacing={1} sx={{ marginTop: 0.1 }}>
<Grid item xs={12} md={3}> <Grid item sx={{ display: !installationId ? 'block' : 'none' }}>
<Card> <Card>
<Divider />
<TableContainer> <TableContainer>
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell padding="checkbox"></TableCell>
<TableCell> <TableCell>
<FormattedMessage id="name" defaultMessage="Name" /> <FormattedMessage id="name" defaultMessage="Name" />
</TableCell> </TableCell>
<TableCell> <TableCell>
<FormattedMessage id="location" defaultMessage="Location" /> <FormattedMessage id="location" defaultMessage="Location" />
</TableCell> </TableCell>
<TableCell>
<FormattedMessage id="country" defaultMessage="Country" />
</TableCell>
<TableCell>
<FormattedMessage
id="order"
defaultMessage="Order Values"
/>
</TableCell>
<TableCell> <TableCell>
<FormattedMessage id="status" defaultMessage="Status" /> <FormattedMessage id="status" defaultMessage="Status" />
</TableCell> </TableCell>
@ -76,7 +100,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
<TableBody> <TableBody>
{props.installations.map((installation) => { {props.installations.map((installation) => {
const isInstallationSelected = const isInstallationSelected =
installation.id === selectedInstallation ? true : false; installation.id === selectedInstallation;
const status = getStatus(installation.id); const status = getStatus(installation.id);
@ -100,31 +124,58 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
onMouseEnter={() => handleRowMouseEnter(installation.id)} onMouseEnter={() => handleRowMouseEnter(installation.id)}
onMouseLeave={() => handleRowMouseLeave()} onMouseLeave={() => handleRowMouseLeave()}
> >
<TableCell padding="checkbox"></TableCell>
<TableCell> <TableCell>
<Typography <Typography
variant="body1" variant="body2"
fontWeight="bold" fontWeight="bold"
color="text.primary" color="text.primary"
gutterBottom gutterBottom
noWrap noWrap
sx={{ marginTop: '10px' }} sx={{ marginTop: '10px', fontSize: 'small' }}
> >
{installation.name} {installation.name}
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Typography <Typography
variant="body1" variant="body2"
fontWeight="bold" fontWeight="bold"
color="text.primary" color="text.primary"
gutterBottom gutterBottom
noWrap noWrap
sx={{ marginTop: '10px' }} sx={{ marginTop: '10px', fontSize: 'small' }}
> >
{installation.location} {installation.location}
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell>
<Typography
variant="body2"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px', fontSize: 'small' }}
>
{installation.country}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body2"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px', fontSize: 'small' }}
>
{installation.orderNumbers}
</Typography>
</TableCell>
<TableCell> <TableCell>
<div <div
style={{ style={{
@ -161,6 +212,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
style={{ style={{
width: '20px', width: '20px',
height: '20px', height: '20px',
marginLeft: '2px',
borderRadius: '50%', borderRadius: '50%',
backgroundColor: backgroundColor:
status === 2 status === 2
@ -182,15 +234,11 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
</TableContainer> </TableContainer>
</Card> </Card>
</Grid> </Grid>
{props.installations.map((installation) => ( {props.installations.map((installation) => (
<Installation <Installation
key={installation.id} key={installation.id}
current_installation={findInstallation(installation.id)} current_installation={findInstallation(installation.id)}
type="installation" type="installation"
style={{
display: installation.id === selectedInstallation ? 'block' : 'none'
}}
></Installation> ></Installation>
))} ))}
</Grid> </Grid>

View File

@ -25,45 +25,56 @@ import Access from '../ManageAccess/Access';
import Log from 'src/content/dashboards/Log/Log'; import Log from 'src/content/dashboards/Log/Log';
import { TimeSpan, UnixTime } from 'src/dataCache/time'; import { TimeSpan, UnixTime } from 'src/dataCache/time';
import { FetchResult } from 'src/dataCache/dataCache'; import { FetchResult } from 'src/dataCache/dataCache';
import { DataRecord } from 'src/dataCache/data'; import {
import { S3Access } from 'src/dataCache/S3/S3Access'; extractValues,
import { parseCsv } from 'src/content/dashboards/Log/graph.util'; TopologyValues
import { I_S3Credentials, Notification } from 'src/interfaces/S3Types'; } from 'src/content/dashboards/Log/graph.util';
import { Notification } from 'src/interfaces/S3Types';
import { LogContext } from 'src/contexts/LogContextProvider'; import { LogContext } from 'src/contexts/LogContextProvider';
import LiveView from '../LiveView/LiveView'; import Topology from '../Topology/Topology';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Overview from '../Overview/overview';
import Configuration from '../Configuration/Configuration';
import { fetchData } from 'src/content/dashboards/Installations/fetchData';
interface singleInstallationProps { interface singleInstallationProps {
current_installation: I_Installation; current_installation: I_Installation;
type: string; type: string;
style?: React.CSSProperties;
} }
function Installation(props: singleInstallationProps) { function Installation(props: singleInstallationProps) {
const tabs = [ const tabs = [
{ {
value: 'installation', value: 'live',
label: ( label: <FormattedMessage id="live" defaultMessage="Live" />
<FormattedMessage id="installation" defaultMessage="Installation" />
)
}, },
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
,
{ {
value: 'manage', value: 'manage',
label: ( label: <FormattedMessage id="manage" defaultMessage="Access Management" />
<FormattedMessage id="manageAccess" defaultMessage="Manage Access" />
)
},
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live View" />
}, },
{ {
value: 'log', value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" /> label: <FormattedMessage id="log" defaultMessage="Log" />
},
{
value: 'information',
label: <FormattedMessage id="information" defaultMessage="Information" />
},
{
value: 'configuration',
label: (
<FormattedMessage id="configuration" defaultMessage="Configuration" />
)
} }
]; ];
const theme = useTheme(); const theme = useTheme();
const [currentTab, setCurrentTab] = useState<string>(tabs[0].value); const [currentTab, setCurrentTab] = useState<string>('live');
const [formValues, setFormValues] = useState(props.current_installation); const [formValues, setFormValues] = useState(props.current_installation);
const requiredFields = ['name', 'region', 'location', 'country']; const requiredFields = ['name', 'region', 'location', 'country'];
const context = useContext(UserContext); const context = useContext(UserContext);
@ -87,37 +98,9 @@ function Installation(props: singleInstallationProps) {
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false); const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
const logContext = useContext(LogContext); const logContext = useContext(LogContext);
const { installationStatus, handleLogWarningOrError, getStatus } = logContext; const { installationStatus, handleLogWarningOrError, getStatus } = logContext;
const searchParams = new URLSearchParams(location.search);
const fetchData = ( const installationId = parseInt(searchParams.get('installation'));
timestamp: UnixTime, const [values, setValues] = useState<TopologyValues | null>(null);
s3Credentials: I_S3Credentials
): Promise<FetchResult<DataRecord>> => {
const s3Path = `${timestamp.ticks}.csv`;
if (s3Credentials && s3Credentials.s3Bucket) {
const s3Access = new S3Access(
s3Credentials.s3Bucket,
s3Credentials.s3Region,
s3Credentials.s3Provider,
s3Credentials.s3Key,
s3Credentials.s3Secret
);
return s3Access
.get(s3Path)
.then(async (r) => {
if (r.status === 404) {
return Promise.resolve(FetchResult.notAvailable);
} else if (r.status === 200) {
const text = await r.text();
return parseCsv(text);
} else {
return Promise.resolve(FetchResult.notAvailable);
}
})
.catch((e) => {
return Promise.resolve(FetchResult.tryLater);
});
}
};
if (formValues == undefined) { if (formValues == undefined) {
return null; return null;
@ -156,9 +139,6 @@ function Installation(props: singleInstallationProps) {
return true; return true;
}; };
useEffect(() => {
setFormValues(props.current_installation);
const S3data = { const S3data = {
s3Region: props.current_installation.s3Region, s3Region: props.current_installation.s3Region,
s3Provider: props.current_installation.s3Provider, s3Provider: props.current_installation.s3Provider,
@ -169,43 +149,89 @@ function Installation(props: singleInstallationProps) {
const s3Bucket = const s3Bucket =
props.current_installation.id.toString() + props.current_installation.id.toString() +
'-3e5b3069-214a-43ee-8d85-57d72000c19d'; '-3e5b3069-214a-43ee-8d85-57d72000c19d';
const s3Credentials = { s3Bucket, ...S3data }; const s3Credentials = { s3Bucket, ...S3data };
useEffect(() => {
let isMounted = true;
setFormValues(props.current_installation);
setErrorLoadingS3Data(false); setErrorLoadingS3Data(false);
const interval = setInterval(() => { const fetchDataPeriodically = async () => {
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20)); const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
const date = now.toDate();
fetchData(now, s3Credentials) try {
.then((res) => { const res = await fetchData(now, s3Credentials);
if (installationId == 2) {
console.log('Fetched data from unix timestamp ' + now); console.log('Fetched data from unix timestamp ' + now);
}
if (!isMounted) {
return;
}
const newWarnings: Notification[] = []; const newWarnings: Notification[] = [];
const newErrors: Notification[] = []; const newErrors: Notification[] = [];
if (res === FetchResult.notAvailable || res == FetchResult.tryLater) {
if (res === FetchResult.notAvailable || res === FetchResult.tryLater) {
setErrorLoadingS3Data(true); setErrorLoadingS3Data(true);
handleLogWarningOrError(props.current_installation.id, -1); handleLogWarningOrError(props.current_installation.id, -1);
} else { } else {
setErrorLoadingS3Data(false); setErrorLoadingS3Data(false);
setValues(
extractValues({
time: now,
value: res
})
);
for (const key in res) { for (const key in res) {
if ( if (
(res.hasOwnProperty(key) && (res.hasOwnProperty(key) &&
key.includes('/Alarms') && key.includes('/Alarms') &&
res[key].value != '') || res[key].value !== '') ||
(key.includes('/Warnings') && res[key].value != '') (key.includes('/Warnings') && res[key].value !== '')
) { ) {
if (key.includes('/Warnings')) { if (key.includes('/Warnings')) {
newWarnings.push({ newWarnings.push({
key, device: key.substring(1, key.lastIndexOf('/')),
value: res[key].value.toString() description: res[key].value.toString(),
date:
date.getFullYear() +
'-' +
date.getMonth() +
'-' +
date.getDay(),
time:
date.getHours() +
':' +
date.getMinutes() +
':' +
date.getSeconds()
}); });
} else if (key.includes('/Alarms')) { } else if (key.includes('/Alarms')) {
newErrors.push({ newErrors.push({
key, device: key.substring(1, key.lastIndexOf('/')),
value: res[key].value.toString() description: res[key].value.toString(),
date:
date.getFullYear() +
'-' +
date.getMonth() +
'-' +
date.getDay(),
time:
date.getHours() +
':' +
date.getMinutes() +
':' +
date.getSeconds()
}); });
} }
} }
} }
setWarnings(newWarnings); setWarnings(newWarnings);
setErrors(newErrors); setErrors(newErrors);
@ -217,16 +243,23 @@ function Installation(props: singleInstallationProps) {
handleLogWarningOrError(props.current_installation.id, 0); handleLogWarningOrError(props.current_installation.id, 0);
} }
} }
}) } catch (err) {
.catch((err) => {
setErrorLoadingS3Data(true); setErrorLoadingS3Data(true);
}); }
}, 2000); };
const interval = setInterval(fetchDataPeriodically, 2000);
// Cleanup function to cancel interval and update isMounted when unmounted
return () => {
isMounted = false;
clearInterval(interval);
};
}, []); }, []);
if (installationId == props.current_installation.id) {
return ( return (
<> <Grid item xs={12} md={12}>
<Grid item xs={12} md={9} style={props.style}>
<TabsContainerWrapper> <TabsContainerWrapper>
<Tabs <Tabs
onChange={handleTabsChange} onChange={handleTabsChange}
@ -249,7 +282,7 @@ function Installation(props: singleInstallationProps) {
alignItems="stretch" alignItems="stretch"
spacing={0} spacing={0}
> >
{currentTab === 'installation' && ( {currentTab === 'information' && (
<Container maxWidth="xl"> <Container maxWidth="xl">
<Grid <Grid
container container
@ -350,6 +383,44 @@ function Installation(props: singleInstallationProps) {
fullWidth fullWidth
/> />
</div> </div>
{currentUser.hasWriteAccess && (
<>
<div>
<TextField
label="S3 Write Key"
name="s3writekey"
value={formValues.s3WriteKey}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label="S3 Write Secret Key"
name="s3writesecretkey"
value={formValues.s3WriteSecret}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label="S3 Bucket Name"
name="s3writesecretkey"
value={
formValues.id +
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
}
variant="outlined"
fullWidth
/>
</div>
</>
)}
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@ -410,7 +481,7 @@ function Installation(props: singleInstallationProps) {
<IconButton <IconButton
color="inherit" color="inherit"
size="small" size="small"
onClick={() => setError(false)} // Set error state to false on click onClick={() => setError(false)}
sx={{ marginLeft: '4px' }} sx={{ marginLeft: '4px' }}
> >
<CloseIcon fontSize="small" /> <CloseIcon fontSize="small" />
@ -448,6 +519,12 @@ function Installation(props: singleInstallationProps) {
</Grid> </Grid>
</Container> </Container>
)} )}
{currentTab === 'overview' && (
<Overview s3Credentials={s3Credentials}></Overview>
)}
{currentTab === 'configuration' && currentUser.hasWriteAccess && (
<Configuration values={values}></Configuration>
)}
{currentTab === 'manage' && currentUser.hasWriteAccess && ( {currentTab === 'manage' && currentUser.hasWriteAccess && (
<AccessContextProvider> <AccessContextProvider>
<Access <Access
@ -456,13 +533,7 @@ function Installation(props: singleInstallationProps) {
></Access> ></Access>
</AccessContextProvider> </AccessContextProvider>
)} )}
{currentTab === 'live' && ( {currentTab === 'live' && <Topology values={values}></Topology>}
<LiveView
warnings={warnings}
errors={errors}
errorLoadingS3Data={errorLoadingS3Data}
></LiveView>
)}
{currentTab === 'log' && ( {currentTab === 'log' && (
<Log <Log
warnings={warnings} warnings={warnings}
@ -473,8 +544,10 @@ function Installation(props: singleInstallationProps) {
</Grid> </Grid>
</Card> </Card>
</Grid> </Grid>
</>
); );
} else {
return null;
}
} }
export default Installation; export default Installation;

View File

@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { import {
FormControl, FormControl,
Grid, Grid,
@ -9,32 +9,42 @@ import {
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone'; import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
import FlatInstallationView from 'src/content/dashboards/Installations/FlatInstallationView'; import FlatInstallationView from 'src/content/dashboards/Installations/FlatInstallationView';
import LogContextProvider from 'src/contexts/LogContextProvider';
function InstallationSearch() { function InstallationSearch() {
const theme = useTheme(); const theme = useTheme();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const { data, fetchAllInstallations } = useContext(InstallationsContext); const { installations, fetchAllInstallations } =
useContext(InstallationsContext);
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
useEffect(() => { useEffect(() => {
fetchAllInstallations(); fetchAllInstallations();
}, []); }, []);
const [filteredData, setFilteredData] = useState(data); const [filteredData, setFilteredData] = useState(installations);
useEffect(() => { useEffect(() => {
const filtered = data.filter( const filtered = installations.filter(
(item) => (item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase()) || item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.location.toLowerCase().includes(searchTerm.toLowerCase()) item.location.toLowerCase().includes(searchTerm.toLowerCase())
); );
setFilteredData(filtered); setFilteredData(filtered);
}, [searchTerm, data]); }, [searchTerm, installations]);
return ( return (
<> <>
<Grid container spacing={4}> <Grid container>
<Grid item xs={12} md={3}> <Grid
<FormControl variant="outlined" fullWidth> item
xs={12}
md={4}
sx={{ display: !installationId ? 'block' : 'none' }}
>
<FormControl variant="outlined">
<TextField <TextField
placeholder="Search" placeholder="Search"
value={searchTerm} value={searchTerm}
@ -51,7 +61,9 @@ function InstallationSearch() {
</FormControl> </FormControl>
</Grid> </Grid>
</Grid> </Grid>
<LogContextProvider>
<FlatInstallationView installations={filteredData} /> <FlatInstallationView installations={filteredData} />
</LogContextProvider>
</> </>
); );
} }

View File

@ -1,25 +1,30 @@
import { ChangeEvent, useState } from 'react'; import React, { ChangeEvent, useEffect, useState } from 'react';
import Footer from 'src/components/Footer'; import Footer from 'src/components/Footer';
import { Box, Card, Container, Grid, Tab, Tabs, useTheme } from '@mui/material'; import { Card, Container, Grid, Tab, Tabs, useTheme } from '@mui/material';
import InstallationsContextProvider from 'src/contexts/InstallationsContextProvider';
import InstallationSearch from './InstallationSearch';
import ListIcon from '@mui/icons-material/List'; import ListIcon from '@mui/icons-material/List';
import AccountTreeIcon from '@mui/icons-material/AccountTree'; import AccountTreeIcon from '@mui/icons-material/AccountTree';
import InstallationTree from '../Tree/InstallationTree';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper'; import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import UsersContextProvider from 'src/contexts/UsersContextProvider'; import UsersContextProvider from 'src/contexts/UsersContextProvider';
import LogContextProvider from '../../../contexts/LogContextProvider'; import {
Link,
Route,
Routes,
useLocation,
useNavigate
} from 'react-router-dom';
import FlatView from './flatView';
import TreeView from '../Tree/treeView';
import routes from 'src/Resources/routes.json';
function InstallationTabs() { function InstallationTabs() {
const theme = useTheme(); const theme = useTheme();
const location = useLocation();
const [currentTab, setCurrentTab] = useState<string>('flat'); const navigate = useNavigate();
const tabs = [ const tabs = [
{ {
value: 'flat', value: 'list',
label: 'Flat view', label: 'Flat view',
icon: <ListIcon id="mode-toggle-button-list-icon" />, icon: <ListIcon id="mode-toggle-button-list-icon" />
component: {}
}, },
{ {
value: 'tree', value: 'tree',
@ -27,9 +32,25 @@ function InstallationTabs() {
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" /> icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
} }
]; ];
const [currentTab, setCurrentTab] = useState<string>('list');
useEffect(() => {
//console.log(location.pathname);
if (
location.pathname === '/installations' ||
location.pathname === '/installations/'
) {
navigate(routes.installations + routes.list, {
replace: true
});
} else if (location.pathname === '/installations/tree') {
setCurrentTab('tree');
}
}, [location.pathname, navigate]);
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => { const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value); setCurrentTab(value);
navigate(value);
}; };
return ( return (
@ -45,12 +66,16 @@ function InstallationTabs() {
indicatorColor="primary" indicatorColor="primary"
> >
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab key={tab.value} value={tab.value} icon={tab.icon} /> <Tab
key={tab.value}
value={tab.value}
icon={tab.icon}
component={Link}
to={routes[tab.value]}
/>
))} ))}
</Tabs> </Tabs>
</TabsContainerWrapper> </TabsContainerWrapper>
<InstallationsContextProvider>
<LogContextProvider>
<Card variant="outlined"> <Card variant="outlined">
<Grid <Grid
container container
@ -59,26 +84,12 @@ function InstallationTabs() {
alignItems="stretch" alignItems="stretch"
spacing={0} spacing={0}
> >
{currentTab === 'tree' && ( <Routes>
<> <Route path={routes.list + '*'} element={<FlatView />} />
<Grid item xs={12}> <Route path={routes.tree + '*'} element={<TreeView />} />
<Box p={4}> </Routes>
<InstallationTree />
</Box>
</Grid>
</>
)}
{currentTab === 'flat' && (
<Grid item xs={12}>
<Box p={4}>
<InstallationSearch />
</Box>
</Grid>
)}
</Grid> </Grid>
</Card> </Card>
</LogContextProvider>
</InstallationsContextProvider>
</Container> </Container>
<Footer /> <Footer />
</UsersContextProvider> </UsersContextProvider>

View File

@ -13,6 +13,7 @@ import { Close as CloseIcon } from '@mui/icons-material';
import { I_Installation } from 'src/interfaces/InstallationTypes'; import { I_Installation } from 'src/interfaces/InstallationTypes';
import { TokenContext } from 'src/contexts/tokenContext'; import { TokenContext } from 'src/contexts/tokenContext';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import { FormattedMessage } from 'react-intl';
interface installationFormProps { interface installationFormProps {
cancel: () => void; cancel: () => void;
@ -96,7 +97,12 @@ function installationForm(props: installationFormProps) {
> >
<div> <div>
<TextField <TextField
label="Customer Name" label={
<FormattedMessage
id="customerName"
defaultMessage="Customer Name"
/>
}
name="name" name="name"
value={formValues.name} value={formValues.name}
onChange={handleChange} onChange={handleChange}
@ -107,7 +113,7 @@ function installationForm(props: installationFormProps) {
</div> </div>
<div> <div>
<TextField <TextField
label="Region" label={<FormattedMessage id="region" defaultMessage="Region" />}
name="region" name="region"
value={formValues.region} value={formValues.region}
onChange={handleChange} onChange={handleChange}
@ -118,7 +124,9 @@ function installationForm(props: installationFormProps) {
</div> </div>
<div> <div>
<TextField <TextField
label="Location" label={
<FormattedMessage id="location" defaultMessage="Location" />
}
name="location" name="location"
value={formValues.location} value={formValues.location}
onChange={handleChange} onChange={handleChange}
@ -130,7 +138,9 @@ function installationForm(props: installationFormProps) {
<div> <div>
<TextField <TextField
label="Country" label={
<FormattedMessage id="country" defaultMessage="Country" />
}
name="country" name="country"
value={formValues.country} value={formValues.country}
onChange={handleChange} onChange={handleChange}
@ -141,7 +151,12 @@ function installationForm(props: installationFormProps) {
</div> </div>
<div> <div>
<TextField <TextField
label="Order Numbers" label={
<FormattedMessage
id="orderNumbers"
defaultMessage="Order Numbers"
/>
}
name="orderNumbers" name="orderNumbers"
value={formValues.orderNumbers} value={formValues.orderNumbers}
onChange={handleChange} onChange={handleChange}
@ -164,7 +179,7 @@ function installationForm(props: installationFormProps) {
}} }}
disabled={!areRequiredFieldsFilled()} disabled={!areRequiredFieldsFilled()}
> >
Submit <FormattedMessage id="submit" defaultMessage="Submit" />
</Button> </Button>
<Button <Button
@ -174,7 +189,7 @@ function installationForm(props: installationFormProps) {
marginLeft: '10px' marginLeft: '10px'
}} }}
> >
Cancel <FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button> </Button>
{loading && ( {loading && (
@ -195,11 +210,14 @@ function installationForm(props: installationFormProps) {
alignItems: 'center' alignItems: 'center'
}} }}
> >
An error has occurred <FormattedMessage
id="errorOccured"
defaultMessage="An error has occured"
/>
<IconButton <IconButton
color="inherit" color="inherit"
size="small" size="small"
onClick={() => setError(false)} // Set error state to false on click onClick={() => setError(false)}
sx={{ marginLeft: '4px' }} sx={{ marginLeft: '4px' }}
> >
<CloseIcon fontSize="small" /> <CloseIcon fontSize="small" />

View File

@ -1,184 +0,0 @@
import React, { Fragment } from 'react';
import {
Alert,
Container,
Divider,
Grid,
IconButton,
ListItem,
useTheme
} from '@mui/material';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import Avatar from '@mui/material/Avatar';
import ListItemText from '@mui/material/ListItemText';
import WarningIcon from '@mui/icons-material/Warning';
import ErrorIcon from '@mui/icons-material/Error';
import Typography from '@mui/material/Typography';
import { Notification } from 'src/interfaces/S3Types';
interface LiveViewProps {
warnings: Notification[];
errors: Notification[];
errorLoadingS3Data: boolean;
}
function LiveView(props: LiveViewProps) {
const theme = useTheme();
return (
<Container maxWidth="xl">
<Grid container>
<Grid item xs={12} md={12}>
{props.errors.length > 0 &&
props.errors.map((error) => {
return (
<Fragment key={error.key + error.value}>
<ListItem>
<ListItemAvatar>
<Avatar
sx={{
backgroundColor: 'red',
width: 28,
height: 28
}}
>
<ErrorIcon
sx={{
color: 'white',
width: 16,
height: 16
}}
/>
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="body1" sx={{ fontWeight: 'bold' }}>
{'Error from: ' +
error.key +
' device: ' +
error.value}
</Typography>
}
/>
</ListItem>
<Divider />
</Fragment>
);
})}
{!props.errorLoadingS3Data && props.errors.length == 0 && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center',
marginTop: '20px',
marginBottom: '20px'
}}
>
There are no errors
<IconButton
color="inherit"
size="small"
sx={{ marginLeft: '4px' }}
></IconButton>
</Alert>
)}
</Grid>
<Grid item xs={12} md={12}>
{props.errors.length > 0 && props.warnings.length > 0 && (
<Divider
sx={{
borderBottomWidth: '2px',
borderColor: '#ffffff',
'&.MuiDivider-root': {
backgroundColor: theme.palette.secondary.light // Change the color as needed
}
}}
/>
)}
</Grid>
<Grid item xs={12} md={12} style={{ marginBottom: '20px' }}>
{props.errorLoadingS3Data && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center',
marginTop: '20px'
//marginBottom: '20px'
}}
>
Cannot load logging data
<IconButton
color="inherit"
size="small"
sx={{ marginLeft: '4px' }}
></IconButton>
</Alert>
)}
{props.warnings.length > 0 &&
props.warnings.map((warning) => {
return (
<Fragment key={warning.key + warning.value}>
<ListItem>
<ListItemAvatar>
<Avatar
sx={{
backgroundColor: '#ffc04d',
width: 28,
height: 28
}}
>
<WarningIcon
sx={{
color: '#ffffff',
width: 16,
height: 16
}}
/>
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="body1" sx={{ fontWeight: 'bold' }}>
{'Warning from: ' +
warning.key +
' device: ' +
warning.value}
</Typography>
}
/>
</ListItem>
<Divider />
</Fragment>
);
})}
{!props.errorLoadingS3Data && props.warnings.length == 0 && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center',
marginBottom: '20px'
}}
>
There are no warnings
<IconButton
color="inherit"
size="small"
sx={{ marginLeft: '4px' }}
></IconButton>
</Alert>
)}
</Grid>
</Grid>
</Container>
);
}
export default LiveView;

View File

@ -1,20 +1,24 @@
import React, { Fragment } from 'react'; import React from 'react';
import { import {
Alert, Alert,
Card,
Container, Container,
Divider, Divider,
Grid, Grid,
IconButton, IconButton,
ListItem, Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import Avatar from '@mui/material/Avatar';
import ListItemText from '@mui/material/ListItemText';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import ErrorIcon from '@mui/icons-material/Error';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { Notification } from 'src/interfaces/S3Types'; import { Notification } from 'src/interfaces/S3Types';
import { FormattedMessage } from 'react-intl';
import ErrorIcon from '@mui/icons-material/Error';
interface LogProps { interface LogProps {
warnings: Notification[]; warnings: Notification[];
@ -29,43 +33,171 @@ function Log(props: LogProps) {
<Container maxWidth="xl"> <Container maxWidth="xl">
<Grid container> <Grid container>
<Grid item xs={12} md={12}> <Grid item xs={12} md={12}>
{props.errors.length > 0 && {(props.errors.length > 0 || props.warnings.length > 0) && (
props.errors.map((error) => { <Card>
<Divider />
<TableContainer>
<Table sx={{ height: 10 }}>
<TableHead>
<TableRow>
<TableCell>
<FormattedMessage id="type" defaultMessage="Type" />
</TableCell>
<TableCell>
<FormattedMessage id="device" defaultMessage="Device" />
</TableCell>
<TableCell>
<FormattedMessage
id="description"
defaultMessage="Description"
/>
</TableCell>
<TableCell>
<FormattedMessage id="date" defaultMessage="Date" />
</TableCell>
<TableCell>
<FormattedMessage id="time" defaultMessage="Time" />
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.errors.map((error, index) => {
return ( return (
<Fragment key={error.key + error.value}> <TableRow hover key={index}>
<ListItem> <TableCell>
<ListItemAvatar>
<Avatar
sx={{
backgroundColor: 'red',
width: 28,
height: 28
}}
>
<ErrorIcon <ErrorIcon
sx={{ sx={{
color: 'white', color: 'red',
width: 16, width: 25,
height: 16 height: 25,
marginLeft: '5px',
marginTop: '8px'
}} }}
/> />
</Avatar> </TableCell>
</ListItemAvatar> <TableCell>
<ListItemText <Typography
primary={ variant="body1"
<Typography variant="body1" sx={{ fontWeight: 'bold' }}> fontWeight="bold"
{'Error from: ' + color="text.primary"
error.key + gutterBottom
' device: ' + noWrap
error.value} sx={{ marginTop: '10px' }}
>
{error.device}
</Typography> </Typography>
} </TableCell>
/> <TableCell>
</ListItem> <Typography
<Divider /> variant="body1"
</Fragment> fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px' }}
>
{error.description}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px' }}
>
{error.date}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px' }}
>
{error.time}
</Typography>
</TableCell>
</TableRow>
); );
})} })}
{props.warnings.map((warning, index) => {
return (
<TableRow hover key={index}>
<TableCell>
<WarningIcon
sx={{
color: '#ffc04d',
width: 25,
height: 25,
marginLeft: '5px',
marginTop: '8px'
}}
/>
</TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px' }}
>
{warning.device}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px' }}
>
{warning.description}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px' }}
>
{warning.date}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px' }}
>
{warning.time}
</Typography>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</Card>
)}
{!props.errorLoadingS3Data && props.errors.length == 0 && ( {!props.errorLoadingS3Data && props.errors.length == 0 && (
<Alert <Alert
severity="error" severity="error"
@ -77,7 +209,10 @@ function Log(props: LogProps) {
marginBottom: '20px' marginBottom: '20px'
}} }}
> >
There are no errors <FormattedMessage
id="noerrors"
defaultMessage="There are no errors"
/>
<IconButton <IconButton
color="inherit" color="inherit"
size="small" size="small"
@ -86,19 +221,7 @@ function Log(props: LogProps) {
</Alert> </Alert>
)} )}
</Grid> </Grid>
<Grid item xs={12} md={12}>
{props.errors.length > 0 && props.warnings.length > 0 && (
<Divider
sx={{
borderBottomWidth: '2px',
borderColor: '#ffffff',
'&.MuiDivider-root': {
backgroundColor: theme.palette.secondary.light // Change the color as needed
}
}}
/>
)}
</Grid>
<Grid item xs={12} md={12} style={{ marginBottom: '20px' }}> <Grid item xs={12} md={12} style={{ marginBottom: '20px' }}>
{props.errorLoadingS3Data && ( {props.errorLoadingS3Data && (
<Alert <Alert
@ -108,10 +231,12 @@ function Log(props: LogProps) {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
marginTop: '20px' marginTop: '20px'
//marginBottom: '20px'
}} }}
> >
Cannot load logging data <FormattedMessage
id="cannotloadloggingdata"
defaultMessage="Cannot load logging data"
/>
<IconButton <IconButton
color="inherit" color="inherit"
size="small" size="small"
@ -119,43 +244,6 @@ function Log(props: LogProps) {
></IconButton> ></IconButton>
</Alert> </Alert>
)} )}
{props.warnings.length > 0 &&
props.warnings.map((warning) => {
return (
<Fragment key={warning.key + warning.value}>
<ListItem>
<ListItemAvatar>
<Avatar
sx={{
backgroundColor: '#ffc04d',
width: 28,
height: 28
}}
>
<WarningIcon
sx={{
color: '#ffffff',
width: 16,
height: 16
}}
/>
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="body1" sx={{ fontWeight: 'bold' }}>
{'Warning from: ' +
warning.key +
' device: ' +
warning.value}
</Typography>
}
/>
</ListItem>
<Divider />
</Fragment>
);
})}
{!props.errorLoadingS3Data && props.warnings.length == 0 && ( {!props.errorLoadingS3Data && props.warnings.length == 0 && (
<Alert <Alert
@ -163,11 +251,14 @@ function Log(props: LogProps) {
sx={{ sx={{
ml: 1, ml: 1,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center'
marginBottom: '20px' //marginBottom: '20px'
}} }}
> >
There are no warnings <FormattedMessage
id="nowarnings"
defaultMessage="There are no warnings"
/>
<IconButton <IconButton
color="inherit" color="inherit"
size="small" size="small"

View File

@ -1,4 +1,5 @@
import { DataRecord } from 'src/dataCache/data'; import { DataPoint, DataRecord } from 'src/dataCache/data';
import { TimeRange, UnixTime } from 'src/dataCache/time';
export interface I_CsvEntry { export interface I_CsvEntry {
value: string | number; value: string | number;
@ -22,3 +23,173 @@ export const parseCsv = (text: string): DataRecord => {
}) })
.reduce((acc, current) => ({ ...acc, ...current }), {} as DataRecord); .reduce((acc, current) => ({ ...acc, ...current }), {} as DataRecord);
}; };
export interface I_BoxDataValue {
unit: string;
value: string | number;
}
export type BoxData = {
label: string;
values: I_BoxDataValue[];
};
export type TopologyValues = {
grid: BoxData;
gridToAcInConnection: BoxData;
gridBus: BoxData;
islandBus: BoxData;
dcBus: BoxData;
dcBusToDcDcConnection: BoxData;
dcDCToBatteryConnection: BoxData;
battery: BoxData;
dcBusToLoadOnDcConnection: BoxData;
islandBusToLoadOnIslandBusConnection: BoxData;
gridBusToPvOnGridbusConnection: BoxData;
gridBusToLoadOnGridBusConnection: BoxData;
inverter: BoxData;
dcDc: BoxData;
islandBusToInverter: BoxData;
inverterToDcBus: BoxData;
gridBusToIslandBusConnection: BoxData;
pvOnDcBusToDcBusConnection: BoxData;
pvOnIslandBusToIslandBusConnection: BoxData;
minimumSoC: BoxData;
installedDcDcPower: BoxData;
gridSetPoint: BoxData;
maximumDischargePower: BoxData;
calibrationChargeForced: BoxData;
};
type TopologyPaths = { [key in keyof TopologyValues]: string[] };
export const topologyPaths: TopologyPaths = {
grid: [
'/GridMeter/Ac/L1/Power/Active',
'/GridMeter/Ac/L2/Power/Active',
'/GridMeter/Ac/L3/Power/Active'
],
gridToAcInConnection: ['/GridMeter/Ac/Power/Active'],
gridBus: [
'/GridMeter/Ac/L1/Power/Active',
'/GridMeter/Ac/L2/Power/Active',
'/GridMeter/Ac/L3/Power/Active'
],
gridBusToPvOnGridbusConnection: ['/PvOnAcGrid/Power/Active'],
gridBusToLoadOnGridBusConnection: ['/LoadOnAcGrid/Power/Active'],
gridBusToIslandBusConnection: ['/AcGridToAcIsland/Power/Active'],
islandBus: [
'/AcDc/Ac/L1/Power/Active',
'/AcDc/Ac/L2/Power/Active',
'/AcDc/Ac/L3/Power/Active'
],
islandBusToLoadOnIslandBusConnection: ['/LoadOnAcIsland/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'
],
inverterToDcBus: ['/AcDc/Dc/Power'],
dcBus: ['/DcDc/Dc/Link/Voltage'],
dcBusToDcDcConnection: ['/DcDc/Dc/Link/Power'],
pvOnDcBusToDcBusConnection: ['/PvOnDc/Dc/Power'],
dcBusToLoadOnDcConnection: ['/LoadOnDc/Power'],
dcDc: ['/DcDc/Dc/Battery/Voltage'],
dcDCToBatteryConnection: ['/DcDc/Dc/Link/Power'],
battery: [
'/Battery/Soc',
'/Battery/Dc/Voltage',
'/Battery/Dc/Current',
'/Battery/Temperature',
'/Battery/Devices/1/Dc/Voltage',
'/Battery/Devices/2/Dc/Voltage',
'/Battery/Devices/3/Dc/Voltage',
'/Battery/Devices/4/Dc/Voltage',
'/Battery/Devices/5/Dc/Voltage',
'/Battery/Devices/6/Dc/Voltage',
'/Battery/Devices/7/Dc/Voltage',
'/Battery/Devices/8/Dc/Voltage',
'/Battery/Devices/9/Dc/Voltage',
'/Battery/Devices/10/Dc/Voltage'
],
minimumSoC: ['/Config/MinSoc'],
installedDcDcPower: ['/DcDc/SystemControl/TargetSlave'],
gridSetPoint: ['/Config/GridSetPoint'],
maximumDischargePower: ['/Config/MaxBatteryDischargingCurrent'],
calibrationChargeForced: ['/Config/ForceCalibrationCharge']
};
export const extractValues = (
timeSeriesData: DataPoint
): TopologyValues | null => {
const extractedValues: TopologyValues = {} as TopologyValues;
for (const topologyKey of Object.keys(topologyPaths)) {
const paths = topologyPaths[topologyKey];
let topologyValues: { unit: string; value: string | number }[] = [];
//console.log('paths is ', paths);
// Check if any of the specified paths exist in the dataRecord
for (const path of paths) {
//console.log(' path is ', path);
if (timeSeriesData.value.hasOwnProperty(path)) {
//console.log('matching path is ', path);
//console.log(timeSeriesData.value[path]);
topologyValues.push({
unit: timeSeriesData.value[path].unit,
value: timeSeriesData.value[path].value
});
}
}
if (topologyValues.length > 0) {
extractedValues[topologyKey] = {
label: topologyPaths[topologyKey as keyof TopologyValues][0]
.split('/')
.pop(),
values: topologyValues
};
}
}
return extractedValues;
};
export const getHighestConnectionValue = (values: TopologyValues) =>
Object.keys(values)
.filter((value) => value.includes('Connection'))
.reduce((acc, curr) => {
const value = Math.abs(
values[curr as keyof TopologyValues].values[0].value as number
);
return value > acc ? value : acc;
}, 0);
export const getAmount = (
highestConnectionValue: number,
values: I_BoxDataValue[]
) => {
return Math.abs(values[0].value as number) / highestConnectionValue;
};
export const createTimes = (
range: TimeRange,
numberOfNodes: number
): UnixTime[] => {
const oneSpan = range.duration.divide(numberOfNodes);
const roundedRange = TimeRange.fromTimes(
range.start.round(oneSpan),
range.end.round(oneSpan)
);
return roundedRange.sample(oneSpan);
};

View File

@ -17,7 +17,7 @@ import {
import { TokenContext } from 'src/contexts/tokenContext'; import { TokenContext } from 'src/contexts/tokenContext';
import { UserContext } from 'src/contexts/userContext'; import { UserContext } from 'src/contexts/userContext';
import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes'; import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
import axiosConfig from '../../../Resources/axiosConfig'; import axiosConfig from 'src/Resources/axiosConfig';
import ListItemAvatar from '@mui/material/ListItemAvatar'; import ListItemAvatar from '@mui/material/ListItemAvatar';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
@ -26,7 +26,8 @@ import PersonIcon from '@mui/icons-material/Person';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material'; import { Close as CloseIcon } from '@mui/icons-material';
import { UsersContext } from 'src/contexts/UsersContextProvider'; import { UsersContext } from 'src/contexts/UsersContextProvider';
import { AccessContext } from '../../../contexts/AccessContextProvider'; import { AccessContext } from 'src/contexts/AccessContextProvider';
import { FormattedMessage } from 'react-intl';
interface AccessProps { interface AccessProps {
currentResource: I_Folder | I_Installation; currentResource: I_Folder | I_Installation;
@ -157,15 +158,32 @@ function Access(props: AccessProps) {
if (NotGrantedAccessUsers.length > 0) { if (NotGrantedAccessUsers.length > 0) {
setError(true); setError(true);
setErrorMessage(
'Unable to grant access to: ' + NotGrantedAccessUsers.join(', ') const message =
); (
<FormattedMessage
id="unableToGrantAccess"
defaultMessage="Unable to grant access to: "
/>
).props.defaultMessage +
' ' +
NotGrantedAccessUsers.join(', ');
setErrorMessage(message);
} }
if (grantedAccessUsers.length > 0) { if (grantedAccessUsers.length > 0) {
setUpdatedMessage( const message =
'Granted access to users: ' + grantedAccessUsers.join(', ') (
); <FormattedMessage
id="grantedAccessToUsers"
defaultMessage="Granted access to users: "
/>
).props.defaultMessage +
' ' +
grantedAccessUsers.join(', ');
setUpdatedMessage(message);
setUpdated(true); setUpdated(true);
setTimeout(() => { setTimeout(() => {
@ -248,7 +266,10 @@ function Access(props: AccessProps) {
//backgroundColor: 'white' //backgroundColor: 'white'
}} }}
> >
Select users <FormattedMessage
id="selectUsers"
defaultMessage="Select Users"
/>
</InputLabel> </InputLabel>
<Select <Select
multiple multiple
@ -301,7 +322,7 @@ function Access(props: AccessProps) {
}} }}
onClick={handleSubmit} onClick={handleSubmit}
> >
Submit <FormattedMessage id="submit" defaultMessage="Submit" />
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
@ -317,7 +338,7 @@ function Access(props: AccessProps) {
padding: '6px 8px' padding: '6px 8px'
}} }}
> >
Cancel <FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button> </Button>
</Box> </Box>
</Box> </Box>
@ -333,7 +354,7 @@ function Access(props: AccessProps) {
'&:hover': { bgcolor: '#f7b34d' } '&:hover': { bgcolor: '#f7b34d' }
}} }}
> >
Grant Access <FormattedMessage id="grantAccess" defaultMessage="Grant Access" />
</Button> </Button>
</Grid> </Grid>
<Grid item xs={12} md={12}> <Grid item xs={12} md={12}>
@ -342,7 +363,10 @@ function Access(props: AccessProps) {
onClick={handleDirectButtonPressed} onClick={handleDirectButtonPressed}
sx={{ marginTop: '20px' }} sx={{ marginTop: '20px' }}
> >
Users with Direct Access <FormattedMessage
id="UserswithDirectAccess"
defaultMessage="Users with Direct Access"
/>
</Button> </Button>
{directButtonPressed && {directButtonPressed &&
@ -382,8 +406,11 @@ function Access(props: AccessProps) {
marginTop: '20px' marginTop: '20px'
}} }}
> >
There are no users with direct access to this {props.resourceType} <FormattedMessage
. id="noUsersWithDirectAccessToThis"
defaultMessage="No users with direct access to this "
/>
{props.resourceType}
<IconButton <IconButton
color="inherit" color="inherit"
size="small" size="small"
@ -398,7 +425,10 @@ function Access(props: AccessProps) {
onClick={handleInheritedButtonPressed} onClick={handleInheritedButtonPressed}
sx={{ marginTop: '20px', marginBottom: '20px' }} sx={{ marginTop: '20px', marginBottom: '20px' }}
> >
Users with Inherited Access <FormattedMessage
id="UserswithInheritedAccess"
defaultMessage="Users with Inherited Access"
/>
</Button> </Button>
{inheritedButtonPressed && usersWithInheritedAccess.length == 0 && ( {inheritedButtonPressed && usersWithInheritedAccess.length == 0 && (
<Alert <Alert
@ -410,7 +440,10 @@ function Access(props: AccessProps) {
marginBottom: '20px' marginBottom: '20px'
}} }}
> >
There are no users with inherited access to this{' '} <FormattedMessage
id="noUsersWithDirectAccessToThis"
defaultMessage="No users with direct access to this "
/>
{props.resourceType}. {props.resourceType}.
<IconButton <IconButton
color="inherit" color="inherit"

View File

@ -0,0 +1,290 @@
import React, { useState } from 'react';
import { Container, Grid, Switch } from '@mui/material';
import TopologyColumn from './topologyColumn';
import {
getAmount,
getHighestConnectionValue,
TopologyValues
} from '../Log/graph.util';
interface TopologyProps {
values: TopologyValues;
}
function Topology(props: TopologyProps) {
if (props.values === null) {
return null;
}
const highestConnectionValue = getHighestConnectionValue(props.values);
const [showValues, setShowValues] = useState(false);
const handleSwitch = () => () => {
setShowValues(!showValues);
};
return (
<Container maxWidth="xl" style={{ backgroundColor: 'white' }}>
<Grid container>
<Grid
item
xs={12}
md={12}
style={{
marginTop: '10px',
height: '20px',
display: 'flex',
flexDirection: 'row',
alignItems: 'right',
justifyContent: 'right'
}}
>
<Switch
edge="start"
color="secondary"
onChange={handleSwitch()}
sx={{
'& .MuiSwitch-thumb': {
backgroundColor: 'orange'
}
}}
/>
</Grid>
<Grid
item
xs={12}
md={12}
style={{
height: '600px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
}}
>
<TopologyColumn
centerBox={{
title: 'Grid',
data: props.values.grid
}}
centerConnection={{
orientation: 'horizontal',
data: props.values.gridToAcInConnection,
amount: props.values.gridToAcInConnection
? getAmount(
highestConnectionValue,
props.values.gridToAcInConnection.values
)
: 0,
showValues: showValues
}}
isLast={false}
isFirst={true}
/>
<TopologyColumn
topBox={{
title: 'Pv Inverter',
data: props.values.gridBusToPvOnGridbusConnection
}}
topConnection={{
orientation: 'vertical',
position: 'top',
data: props.values.gridBusToPvOnGridbusConnection,
amount: props.values.gridBusToPvOnGridbusConnection
? getAmount(
highestConnectionValue,
props.values.gridBusToPvOnGridbusConnection.values
)
: 0,
showValues: showValues
}}
centerBox={{
title: 'Grid Bus',
data: props.values.gridBus
}}
centerConnection={{
orientation: 'horizontal',
data: props.values.gridBusToIslandBusConnection,
amount: props.values.gridBusToIslandBusConnection
? getAmount(
highestConnectionValue,
props.values.gridBusToIslandBusConnection.values
)
: 0,
showValues: showValues
}}
bottomBox={{
title: 'AC Loads',
data: props.values.gridBusToLoadOnGridBusConnection
}}
bottomConnection={{
orientation: 'vertical',
position: 'bottom',
data: props.values.gridBusToLoadOnGridBusConnection,
amount: props.values.gridBusToLoadOnGridBusConnection
? getAmount(
highestConnectionValue,
props.values.gridBusToLoadOnGridBusConnection.values
)
: 0,
showValues: showValues
}}
isLast={false}
isFirst={false}
/>
<TopologyColumn
topBox={{
title: 'Pv Inverter',
data: props.values.pvOnIslandBusToIslandBusConnection
}}
topConnection={{
orientation: 'vertical',
position: 'top',
data: props.values.pvOnIslandBusToIslandBusConnection,
amount: props.values.pvOnIslandBusToIslandBusConnection
? getAmount(
highestConnectionValue,
props.values.pvOnIslandBusToIslandBusConnection.values
)
: 0,
showValues: showValues
}}
centerBox={{
title: 'Island Bus',
data: props.values.islandBus
}}
centerConnection={{
orientation: 'horizontal',
data: props.values.islandBusToInverter,
amount: props.values.islandBusToInverter
? getAmount(
highestConnectionValue,
props.values.islandBusToInverter.values
)
: 0,
showValues: showValues
}}
bottomBox={{
title: 'AC Loads',
data: props.values.islandBusToLoadOnIslandBusConnection
}}
bottomConnection={{
orientation: 'vertical',
position: 'bottom',
data: props.values.islandBusToLoadOnIslandBusConnection,
amount: props.values.islandBusToLoadOnIslandBusConnection
? getAmount(
highestConnectionValue,
props.values.islandBusToLoadOnIslandBusConnection.values
)
: 0,
showValues: showValues
}}
isLast={false}
isFirst={false}
/>
<TopologyColumn
centerBox={{
title: 'AC-DC',
data: props.values.inverter
}}
centerConnection={{
orientation: 'horizontal',
data: props.values.inverterToDcBus,
amount: props.values.inverterToDcBus
? getAmount(
highestConnectionValue,
props.values.inverterToDcBus.values
)
: 0,
showValues: showValues
}}
isLast={false}
isFirst={false}
/>
<TopologyColumn
topBox={{
title: 'Pv DcDc',
data: props.values.pvOnDcBusToDcBusConnection
}}
topConnection={{
orientation: 'vertical',
position: 'top',
data: props.values.pvOnDcBusToDcBusConnection,
amount: props.values.pvOnDcBusToDcBusConnection
? getAmount(
highestConnectionValue,
props.values.pvOnDcBusToDcBusConnection.values
)
: 0,
showValues: showValues
}}
centerBox={{
title: 'DC Link',
data: props.values.dcBus
}}
centerConnection={{
orientation: 'horizontal',
data: props.values.dcBusToDcDcConnection,
amount: props.values.dcBusToDcDcConnection
? getAmount(
highestConnectionValue,
props.values.dcBusToDcDcConnection.values
)
: 0,
showValues: showValues
}}
bottomBox={{
title: 'DC Loads',
data: props.values.dcBusToLoadOnDcConnection
}}
bottomConnection={{
orientation: 'vertical',
position: 'bottom',
data: props.values.dcBusToLoadOnDcConnection,
amount: props.values.dcBusToLoadOnDcConnection
? getAmount(
highestConnectionValue,
props.values.dcBusToLoadOnDcConnection.values
)
: 0,
showValues: showValues
}}
isLast={false}
isFirst={false}
/>
<TopologyColumn
centerBox={{
title: 'DC-DC',
data: props.values.dcDc
}}
centerConnection={{
orientation: 'horizontal',
data: props.values.dcDCToBatteryConnection,
amount: props.values.dcDCToBatteryConnection
? getAmount(
highestConnectionValue,
props.values.dcDCToBatteryConnection.values
)
: 0,
showValues: showValues
}}
isLast={false}
isFirst={false}
/>
<TopologyColumn
centerBox={{
title: 'Battery',
data: props.values.battery
}}
isLast={true}
isFirst={false}
/>
</Grid>
</Grid>
</Container>
);
}
export default Topology;

View File

@ -1,4 +1,4 @@
import React, { ReactNode } from 'react'; import React, { ReactNode, useContext, useState } from 'react';
import { CircularProgress, ListItemIcon, useTheme } from '@mui/material'; import { CircularProgress, ListItemIcon, useTheme } from '@mui/material';
import { TreeItem } from '@mui/lab'; import { TreeItem } from '@mui/lab';
import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes'; import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
@ -7,13 +7,14 @@ import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { makeStyles } from '@mui/styles'; import { makeStyles } from '@mui/styles';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import { LogContext } from 'src/contexts/LogContextProvider';
import routes from 'src/Resources/routes.json';
import { useNavigate } from 'react-router-dom';
interface CustomTreeItemProps { interface CustomTreeItemProps {
node: I_Installation | I_Folder; node: I_Installation | I_Folder;
parent_id: number; parent_id: number;
children?: ReactNode; children?: ReactNode;
handleSelectedInstallation: () => void;
status: number;
} }
const useTreeItemStyles = makeStyles((theme) => ({ const useTreeItemStyles = makeStyles((theme) => ({
@ -37,6 +38,41 @@ const useTreeItemStyles = makeStyles((theme) => ({
function CustomTreeItem(props: CustomTreeItemProps) { function CustomTreeItem(props: CustomTreeItemProps) {
const theme = useTheme(); const theme = useTheme();
const classes = useTreeItemStyles(); const classes = useTreeItemStyles();
const logContext = useContext(LogContext);
const { getStatus } = logContext;
const status = getStatus(props.node.id);
const navigate = useNavigate();
const [selected, setSelected] = useState(false);
const searchParams = new URLSearchParams(location.search);
const installationId = parseInt(searchParams.get('installation'));
const handleSelectOneInstallation = (): void => {
let installation = props.node;
if (installation.type != 'Folder') {
navigate(
routes.installations +
routes.tree +
'?installation=' +
installation.id.toString(),
{
replace: true
}
);
setSelected(!selected);
} else {
navigate(
routes.installations +
routes.tree +
'?folder=' +
installation.id.toString(),
{
replace: true
}
);
setSelected(false);
}
};
const renderIcon = () => { const renderIcon = () => {
if (props.node.type === 'Folder') { if (props.node.type === 'Folder') {
return <FolderIcon />; return <FolderIcon />;
@ -45,13 +81,14 @@ function CustomTreeItem(props: CustomTreeItemProps) {
} }
return null; return null;
}; };
const itemClasses = [classes.labelRoot];
return ( return (
<TreeItem <TreeItem
nodeId={ nodeId={props.node.id.toString() + props.node.type}
props.node.id.toString() + props.parent_id.toString() + props.node.type
}
label={ label={
<div className={classes.labelRoot}> <div className={itemClasses.join(' ')}>
<ListItemIcon color="inherit" className={classes.labelIcon}> <ListItemIcon color="inherit" className={classes.labelIcon}>
{renderIcon()} {renderIcon()}
</ListItemIcon> </ListItemIcon>
@ -63,16 +100,17 @@ function CustomTreeItem(props: CustomTreeItemProps) {
> >
{props.node.name} {props.node.name}
</Typography> </Typography>
{props.node.type === 'Installation' && ( {props.node.type === 'Installation' && (
<div> <div>
{props.status === -1 ? ( {status === -1 ? (
<CancelIcon <CancelIcon
style={{ style={{
width: '23px', width: '23px',
height: '23px', height: '23px',
color: 'red', color: 'red',
borderRadius: '50%', borderRadius: '50%',
marginRight: '40px', marginLeft: '21px',
marginTop: '30px' marginTop: '30px'
}} }}
/> />
@ -80,12 +118,12 @@ function CustomTreeItem(props: CustomTreeItemProps) {
'' ''
)} )}
{props.status === -2 ? ( {status === -2 ? (
<CircularProgress <CircularProgress
size={20} size={20}
sx={{ sx={{
color: '#f7b34d', color: '#f7b34d',
marginRight: '40px', marginLeft: '20px',
marginTop: '30px' marginTop: '30px'
}} }}
/> />
@ -98,13 +136,13 @@ function CustomTreeItem(props: CustomTreeItemProps) {
width: '20px', width: '20px',
height: '20px', height: '20px',
borderRadius: '50%', borderRadius: '50%',
marginRight: '40px', marginLeft: '20px',
backgroundColor: backgroundColor:
props.status === 2 status === 2
? 'red' ? 'red'
: props.status === 1 : status === 1
? 'orange' ? 'orange'
: props.status === -1 || props.status === -2 : status === -1 || status === -2
? 'transparent' ? 'transparent'
: 'green' : 'green'
}} }}
@ -114,6 +152,7 @@ function CustomTreeItem(props: CustomTreeItemProps) {
</div> </div>
} }
sx={{ sx={{
display: !installationId ? 'block' : 'none',
'.MuiTreeItem-content': { '.MuiTreeItem-content': {
width: 'inherit', width: 'inherit',
@ -123,9 +162,10 @@ function CustomTreeItem(props: CustomTreeItemProps) {
cursor: 'pointer', cursor: 'pointer',
backgroundColor: theme.colors.primary.lighter backgroundColor: theme.colors.primary.lighter
} }
} },
backgroundColor: selected ? '#111111' : '#ffffff'
}} }}
onClick={() => props.handleSelectedInstallation()} onClick={() => handleSelectOneInstallation()}
> >
{props.children} {props.children}
</TreeItem> </TreeItem>

View File

@ -24,6 +24,7 @@ import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import AccessContextProvider from 'src/contexts/AccessContextProvider'; import AccessContextProvider from 'src/contexts/AccessContextProvider';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import Access from '../ManageAccess/Access'; import Access from '../ManageAccess/Access';
import { FormattedMessage } from 'react-intl';
interface singleFolderProps { interface singleFolderProps {
current_folder: I_Folder; current_folder: I_Folder;
@ -43,6 +44,8 @@ function Folder(props: singleFolderProps) {
const [isRowHovered, setHoveredRow] = useState(-1); const [isRowHovered, setHoveredRow] = useState(-1);
const [selectedUser, setSelectedUser] = useState<number>(-1); const [selectedUser, setSelectedUser] = useState<number>(-1);
const selectedBulkActions = selectedUser !== -1; const selectedBulkActions = selectedUser !== -1;
const searchParams = new URLSearchParams(location.search);
const folderId = parseInt(searchParams.get('folder'));
const installationContext = useContext(InstallationsContext); const installationContext = useContext(InstallationsContext);
const { const {
@ -65,8 +68,16 @@ function Folder(props: singleFolderProps) {
} }
const tabs = [ const tabs = [
{ value: 'folder', label: 'Folder' }, {
{ value: 'manage', label: 'Manage Access' } value: 'folder',
label: <FormattedMessage id="folder" defaultMessage="Folder" />
},
{
value: 'manage',
label: (
<FormattedMessage id="manageAccess" defaultMessage="Manage Access" />
)
}
]; ];
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => { const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
@ -140,6 +151,8 @@ function Folder(props: singleFolderProps) {
} }
return true; return true;
}; };
if (folderId == props.current_folder.id) {
return ( return (
<> <>
{openModalFolder && ( {openModalFolder && (
@ -200,7 +213,12 @@ function Folder(props: singleFolderProps) {
> >
<div> <div>
<TextField <TextField
label="Name" label={
<FormattedMessage
id="name"
defaultMessage="Name"
/>
}
name="name" name="name"
value={formValues.name} value={formValues.name}
onChange={handleChange} onChange={handleChange}
@ -211,7 +229,12 @@ function Folder(props: singleFolderProps) {
</div> </div>
<div> <div>
<TextField <TextField
label="Information" label={
<FormattedMessage
id="information"
defaultMessage="Information"
/>
}
name="information" name="information"
value={formValues.information} value={formValues.information}
onChange={handleChange} onChange={handleChange}
@ -236,7 +259,10 @@ function Folder(props: singleFolderProps) {
}} }}
disabled={!areRequiredFieldsFilled()} disabled={!areRequiredFieldsFilled()}
> >
Apply Changes <FormattedMessage
id="applyChanges"
defaultMessage="Apply Changes"
/>
</Button> </Button>
)} )}
@ -248,7 +274,10 @@ function Folder(props: singleFolderProps) {
marginLeft: '10px' marginLeft: '10px'
}} }}
> >
Add new installation <FormattedMessage
id="addNewInstallation"
defaultMessage="Add new installation"
/>
</Button> </Button>
)} )}
@ -260,7 +289,10 @@ function Folder(props: singleFolderProps) {
marginLeft: '10px' marginLeft: '10px'
}} }}
> >
Add new folder <FormattedMessage
id="addNewFolder"
defaultMessage="Add new folder"
/>
</Button> </Button>
)} )}
@ -272,7 +304,10 @@ function Folder(props: singleFolderProps) {
marginLeft: '10px' marginLeft: '10px'
}} }}
> >
Delete Folder <FormattedMessage
id="deleteFolder"
defaultMessage="Delete Folder"
/>
</Button> </Button>
)} )}
@ -293,7 +328,10 @@ function Folder(props: singleFolderProps) {
alignItems: 'center' alignItems: 'center'
}} }}
> >
An error has occurred <FormattedMessage
id="errorOccured"
defaultMessage="An error has occurred"
/>
<IconButton <IconButton
color="inherit" color="inherit"
size="small" size="small"
@ -313,7 +351,10 @@ function Folder(props: singleFolderProps) {
alignItems: 'center' alignItems: 'center'
}} }}
> >
Successfully updated <FormattedMessage
id="successfullyUpdated"
defaultMessage="Successfully updated"
/>
<IconButton <IconButton
color="inherit" color="inherit"
size="small" size="small"
@ -344,6 +385,9 @@ function Folder(props: singleFolderProps) {
</Grid> </Grid>
</> </>
); );
} else {
return null;
}
} }
export default Folder; export default Folder;

View File

@ -1,86 +1,33 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect } from 'react';
import { Grid, useTheme } from '@mui/material'; import { Grid } from '@mui/material';
import { TreeView } from '@mui/lab'; import { TreeView } from '@mui/lab';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import CustomTreeItem from './CustomTreeItem'; import CustomTreeItem from './CustomTreeItem';
import Installation from '../Installations/Installation'; import Installation from '../Installations/Installation';
import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
import Folder from './Folder'; import Folder from './Folder';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import { LogContext } from '../../../contexts/LogContextProvider'; import LogContextProvider from 'src/contexts/LogContextProvider';
function InstallationTree() { function InstallationTree() {
const theme = useTheme(); const { foldersAndInstallations, fetchAllFoldersAndInstallations } =
const { data, fetchAllFoldersAndInstallations } =
useContext(InstallationsContext); useContext(InstallationsContext);
const [selectedInstallation, setSelectedInstallation] = useState<number>(-1);
const [selectedFolder, setSelectedFolder] = useState<number>(-1);
const [installationStatus, setInstallationStatus] = useState<
Record<number, number[]>
>({});
const selectedBulkActionsForFolders = selectedFolder === -1 ? false : true;
useEffect(() => { useEffect(() => {
fetchAllFoldersAndInstallations(); fetchAllFoldersAndInstallations();
}, []); }, []);
const findInstallation = (id: number) => {
return data.find(
(installation) => installation.type != 'Folder' && installation.id === id
) as I_Installation;
};
const findFolder = (id: number) => {
return data.find(
(folder) => folder.type == 'Folder' && folder.id === id
) as I_Folder;
};
const logContext = useContext(LogContext);
const { getStatus } = logContext;
const handleSelectOneInstallation = (
installation: I_Installation | I_Folder
): void => {
if (installation.type != 'Folder') {
if (selectedInstallation != installation.id) {
setSelectedInstallation(installation.id);
setSelectedFolder(-1);
} else {
setSelectedInstallation(-1);
}
} else {
if (selectedFolder != installation.id) {
setSelectedFolder(installation.id);
setSelectedInstallation(-1);
} else {
setSelectedFolder(-1);
}
}
};
const TreeNode = ({ node, parent_id }) => { const TreeNode = ({ node, parent_id }) => {
if (node.type == 'Folder') { if (node.type == 'Folder') {
return ( return (
node.parentId == parent_id && ( node.parentId == parent_id && (
<CustomTreeItem <CustomTreeItem node={node} parent_id={parent_id}>
node={node} {foldersAndInstallations.map((subnode) => {
parent_id={parent_id}
handleSelectedInstallation={() => handleSelectOneInstallation(node)}
status={0}
>
{data.map((subnode) => {
return ( return (
subnode != node && subnode != node &&
subnode.parentId == node.id && ( subnode.parentId == node.id && (
<TreeNode <TreeNode
key={ key={subnode.id.toString() + subnode.type}
subnode.id.toString() +
parent_id.toString() +
subnode.type
}
node={subnode} node={subnode}
parent_id={node.id} parent_id={node.id}
/> />
@ -91,32 +38,27 @@ function InstallationTree() {
) )
); );
} else { } else {
const status = getStatus(node.id);
return ( return (
node.parentId == parent_id && ( node.parentId == parent_id && (
<CustomTreeItem <CustomTreeItem node={node} parent_id={parent_id} />
node={node}
parent_id={parent_id}
handleSelectedInstallation={() => handleSelectOneInstallation(node)}
status={status}
/>
) )
); );
} }
}; };
return ( return (
<LogContextProvider>
<Grid container spacing={1} sx={{ marginTop: 0.1 }}> <Grid container spacing={1} sx={{ marginTop: 0.1 }}>
<Grid item xs={12} md={3}> <Grid item xs={12} md={3}>
<TreeView <TreeView
defaultCollapseIcon={<ExpandMoreIcon />} defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />} defaultExpandIcon={<ChevronRightIcon />}
defaultExpanded={['1Folder']}
> >
{data.map((node, index) => { {foldersAndInstallations.map((node, index) => {
return ( return (
<TreeNode <TreeNode
key={node.id.toString() + node.parentId.toString() + node.type} key={node.id.toString() + node.type}
node={node} node={node}
parent_id={'0'} parent_id={'0'}
/> />
@ -125,26 +67,26 @@ function InstallationTree() {
</TreeView> </TreeView>
</Grid> </Grid>
{data.map((installation) => { {foldersAndInstallations.map((installation) => {
if (installation.type == 'Installation') { if (installation.type == 'Installation') {
return ( return (
<Installation <Installation
key={installation.id} key={installation.id + installation.type}
current_installation={findInstallation(installation.id)} current_installation={installation}
type="tree" type="tree"
style={{
display:
installation.id === selectedInstallation ? 'block' : 'none'
}}
></Installation> ></Installation>
); );
} else {
return (
<Folder
key={installation.id + installation.type}
current_folder={installation}
></Folder>
);
} }
})} })}
{selectedBulkActionsForFolders && (
<Folder current_folder={findFolder(selectedFolder)}></Folder>
)}
</Grid> </Grid>
</LogContextProvider>
); );
} }

View File

@ -13,6 +13,7 @@ import { Close as CloseIcon } from '@mui/icons-material';
import { I_Folder } from 'src/interfaces/InstallationTypes'; import { I_Folder } from 'src/interfaces/InstallationTypes';
import { TokenContext } from 'src/contexts/tokenContext'; import { TokenContext } from 'src/contexts/tokenContext';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider'; import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import { FormattedMessage } from 'react-intl';
interface folderFormProps { interface folderFormProps {
cancel: () => void; cancel: () => void;
@ -94,7 +95,7 @@ function folderForm(props: folderFormProps) {
> >
<div> <div>
<TextField <TextField
label="Name" label={<FormattedMessage id="name" defaultMessage="Name" />}
name="name" name="name"
value={formValues.name} value={formValues.name}
onChange={handleChange} onChange={handleChange}
@ -106,7 +107,12 @@ function folderForm(props: folderFormProps) {
<div> <div>
<TextField <TextField
label="Information" label={
<FormattedMessage
id="information"
defaultMessage="Information"
/>
}
name="information" name="information"
value={formValues.information} value={formValues.information}
onChange={handleChange} onChange={handleChange}
@ -129,7 +135,7 @@ function folderForm(props: folderFormProps) {
}} }}
disabled={!areRequiredFieldsFilled()} disabled={!areRequiredFieldsFilled()}
> >
Submit <FormattedMessage id="submit" defaultMessage="Submit" />
</Button> </Button>
<Button <Button
@ -139,7 +145,7 @@ function folderForm(props: folderFormProps) {
marginLeft: '10px' marginLeft: '10px'
}} }}
> >
Cancel <FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button> </Button>
{loading && ( {loading && (
@ -160,11 +166,14 @@ function folderForm(props: folderFormProps) {
alignItems: 'center' alignItems: 'center'
}} }}
> >
An error has occurred <FormattedMessage
id="errorOccured"
defaultMessage="An error has occurred"
/>
<IconButton <IconButton
color="inherit" color="inherit"
size="small" size="small"
onClick={() => setError(false)} // Set error state to false on click onClick={() => setError(false)}
sx={{ marginLeft: '4px' }} sx={{ marginLeft: '4px' }}
> >
<CloseIcon fontSize="small" /> <CloseIcon fontSize="small" />

View File

@ -12,6 +12,7 @@ import { UsersContext } from '../../../contexts/UsersContextProvider';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import UserForm from './userForm'; import UserForm from './userForm';
import { UserContext } from '../../../contexts/userContext'; import { UserContext } from '../../../contexts/userContext';
import { FormattedMessage } from 'react-intl';
function UsersSearch() { function UsersSearch() {
const theme = useTheme(); const theme = useTheme();
@ -55,7 +56,7 @@ function UsersSearch() {
<Grid item xs={12} md={3}> <Grid item xs={12} md={3}>
{currentUser.hasWriteAccess && ( {currentUser.hasWriteAccess && (
<Button variant="contained" onClick={handleSubmit}> <Button variant="contained" onClick={handleSubmit}>
Create user <FormattedMessage id="addUser" defaultMessage="Create user" />
</Button> </Button>
)} )}
</Grid> </Grid>

View File

@ -20,6 +20,7 @@ import { TokenContext } from 'src/contexts/tokenContext';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes'; import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
import { FormattedMessage } from 'react-intl';
interface userFormProps { interface userFormProps {
cancel: () => void; cancel: () => void;
@ -208,7 +209,7 @@ function userForm(props: userFormProps) {
> >
<div> <div>
<TextField <TextField
label="Name" label={<FormattedMessage id="name" defaultMessage="Name" />}
name="name" name="name"
value={formValues.name} value={formValues.name}
onChange={handleChange} onChange={handleChange}
@ -219,7 +220,7 @@ function userForm(props: userFormProps) {
</div> </div>
<div> <div>
<TextField <TextField
label="Email" label={<FormattedMessage id="email" defaultMessage="Email" />}
name="email" name="email"
value={formValues.email} value={formValues.email}
onChange={handleChange} onChange={handleChange}
@ -230,7 +231,12 @@ function userForm(props: userFormProps) {
</div> </div>
<div> <div>
<TextField <TextField
label="Information" label={
<FormattedMessage
id="information"
defaultMessage="Information"
/>
}
name="information" name="information"
value={formValues.information} value={formValues.information}
onChange={handleChange} onChange={handleChange}
@ -249,7 +255,10 @@ function userForm(props: userFormProps) {
backgroundColor: 'transparent' backgroundColor: 'transparent'
}} }}
> >
Grant access to folders <FormattedMessage
id="grantAccessToFolders"
defaultMessage="Grant access to folders"
/>
</InputLabel> </InputLabel>
<Select <Select
multiple multiple
@ -284,7 +293,7 @@ function userForm(props: userFormProps) {
}} }}
onClick={handleCloseFolder} onClick={handleCloseFolder}
> >
Submit <FormattedMessage id="submit" defaultMessage="Submit" />
</Button> </Button>
</Select> </Select>
</FormControl> </FormControl>
@ -304,7 +313,10 @@ function userForm(props: userFormProps) {
fontSize: 14 fontSize: 14
}} }}
> >
Grant access to installations <FormattedMessage
id="grantAccessToInstallations"
defaultMessage="Grant access to installations"
/>
</InputLabel> </InputLabel>
<Select <Select
multiple multiple
@ -339,7 +351,7 @@ function userForm(props: userFormProps) {
}} }}
onClick={handleCloseInstallation} onClick={handleCloseInstallation}
> >
Submit <FormattedMessage id="submit" defaultMessage="Submit" />
</Button> </Button>
</Select> </Select>
</FormControl> </FormControl>
@ -375,7 +387,7 @@ function userForm(props: userFormProps) {
}} }}
disabled={!areRequiredFieldsFilled()} disabled={!areRequiredFieldsFilled()}
> >
Submit <FormattedMessage id="submit" defaultMessage="Submit" />
</Button> </Button>
<Button <Button
@ -385,7 +397,7 @@ function userForm(props: userFormProps) {
marginLeft: '10px' marginLeft: '10px'
}} }}
> >
Cancel <FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button> </Button>
{loading && ( {loading && (
@ -410,7 +422,7 @@ function userForm(props: userFormProps) {
<IconButton <IconButton
color="inherit" color="inherit"
size="small" size="small"
onClick={() => setError(false)} // Set error state to false on click onClick={() => setError(false)}
sx={{ marginLeft: '4px' }} sx={{ marginLeft: '4px' }}
> >
<CloseIcon fontSize="small" /> <CloseIcon fontSize="small" />

View File

@ -1,4 +1,4 @@
import { import React, {
createContext, createContext,
ReactNode, ReactNode,
useCallback, useCallback,
@ -11,6 +11,7 @@ import {
I_UserWithInheritedAccess, I_UserWithInheritedAccess,
InnovEnergyUser InnovEnergyUser
} from '../interfaces/UserTypes'; } from '../interfaces/UserTypes';
import { FormattedMessage } from 'react-intl';
interface AccessContextProviderProps { interface AccessContextProviderProps {
usersWithDirectAccess: InnovEnergyUser[]; usersWithDirectAccess: InnovEnergyUser[];
@ -82,7 +83,15 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
}) })
.catch((error) => { .catch((error) => {
setError(true); setError(true);
setErrorMessage('Unable to load data');
const message = (
<FormattedMessage
id="unableToLoadData"
defaultMessage="Unable to load data"
/>
).props.defaultMessage;
setErrorMessage(message);
}); });
}, },
[] []
@ -99,7 +108,13 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
}) })
.catch((error) => { .catch((error) => {
setError(true); setError(true);
setErrorMessage('Unable to load data'); const message = (
<FormattedMessage
id="unableToLoadData"
defaultMessage="Unable to load data"
/>
).props.defaultMessage;
setErrorMessage(message);
}); });
}, },
[] []
@ -127,7 +142,19 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
resourceType, resourceType,
current_ResourceId current_ResourceId
); );
setUpdatedMessage('Revoked access from user: ' + name);
const message =
(
<FormattedMessage
id="revokedAccessFromUser"
defaultMessage="Revoked access from user: "
/>
).props.defaultMessage +
' ' +
name;
setUpdatedMessage(message);
setUpdated(true); setUpdated(true);
setTimeout(() => { setTimeout(() => {
setUpdated(false); setUpdated(false);
@ -136,7 +163,14 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
}) })
.catch((error) => { .catch((error) => {
setError(true); setError(true);
setErrorMessage('Unable to revoke access'); const message = (
<FormattedMessage
id="unableToRevokeAccess"
defaultMessage="Unable to revoke access"
/>
).props.defaultMessage;
setErrorMessage(message);
}); });
}, },
[] []

View File

@ -11,7 +11,8 @@ import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
import { TokenContext } from './tokenContext'; import { TokenContext } from './tokenContext';
interface I_InstallationContextProviderProps { interface I_InstallationContextProviderProps {
data: I_Installation[]; installations: I_Installation[];
foldersAndInstallations: I_Installation[];
fetchAllInstallations: () => Promise<void>; fetchAllInstallations: () => Promise<void>;
fetchAllFoldersAndInstallations: () => Promise<void>; fetchAllFoldersAndInstallations: () => Promise<void>;
createInstallation: (value: Partial<I_Installation>) => Promise<void>; createInstallation: (value: Partial<I_Installation>) => Promise<void>;
@ -24,14 +25,14 @@ interface I_InstallationContextProviderProps {
setUpdated: (value: boolean) => void; setUpdated: (value: boolean) => void;
deleteInstallation: (value: I_Installation, view: string) => Promise<void>; deleteInstallation: (value: I_Installation, view: string) => Promise<void>;
createFolder: (value: Partial<I_Folder>) => Promise<void>; createFolder: (value: Partial<I_Folder>) => Promise<void>;
updateFolder: (value: I_Folder) => Promise<void>; updateFolder: (value: I_Folder) => Promise<void>;
deleteFolder: (value: I_Folder) => Promise<void>; deleteFolder: (value: I_Folder) => Promise<void>;
} }
export const InstallationsContext = export const InstallationsContext =
createContext<I_InstallationContextProviderProps>({ createContext<I_InstallationContextProviderProps>({
data: [], installations: [],
foldersAndInstallations: [],
fetchAllInstallations: () => Promise.resolve(), fetchAllInstallations: () => Promise.resolve(),
fetchAllFoldersAndInstallations: () => Promise.resolve(), fetchAllFoldersAndInstallations: () => Promise.resolve(),
createInstallation: () => Promise.resolve(), createInstallation: () => Promise.resolve(),
@ -44,7 +45,6 @@ export const InstallationsContext =
setUpdated: () => {}, setUpdated: () => {},
deleteInstallation: () => Promise.resolve(), deleteInstallation: () => Promise.resolve(),
createFolder: () => Promise.resolve(), createFolder: () => Promise.resolve(),
updateFolder: () => Promise.resolve(), updateFolder: () => Promise.resolve(),
deleteFolder: () => Promise.resolve() deleteFolder: () => Promise.resolve()
}); });
@ -54,7 +54,8 @@ const InstallationsContextProvider = ({
}: { }: {
children: ReactNode; children: ReactNode;
}) => { }) => {
const [data, setData] = useState([]); const [installations, setInstallations] = useState([]);
const [foldersAndInstallations, setFoldersAndInstallations] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [updated, setUpdated] = useState(false); const [updated, setUpdated] = useState(false);
@ -67,7 +68,7 @@ const InstallationsContextProvider = ({
axiosConfig axiosConfig
.get('/GetAllInstallations', {}) .get('/GetAllInstallations', {})
.then((res) => { .then((res) => {
setData(res.data); setInstallations(res.data);
}) })
.catch((err: AxiosError) => { .catch((err: AxiosError) => {
if (err.response && err.response.status == 401) { if (err.response && err.response.status == 401) {
@ -80,14 +81,14 @@ const InstallationsContextProvider = ({
return axiosConfig return axiosConfig
.get('/GetAllFoldersAndInstallations') .get('/GetAllFoldersAndInstallations')
.then((res) => { .then((res) => {
setData(res.data); setFoldersAndInstallations(res.data);
}) })
.catch((err) => { .catch((err) => {
if (err.response && err.response.status == 401) { if (err.response && err.response.status == 401) {
removeToken(); removeToken();
} }
}); });
}, [setData]); }, []);
const createInstallation = useCallback( const createInstallation = useCallback(
async (formValues: Partial<I_Installation>) => { async (formValues: Partial<I_Installation>) => {
@ -235,7 +236,8 @@ const InstallationsContextProvider = ({
return ( return (
<InstallationsContext.Provider <InstallationsContext.Provider
value={{ value={{
data, installations,
foldersAndInstallations,
fetchAllInstallations, fetchAllInstallations,
fetchAllFoldersAndInstallations, fetchAllFoldersAndInstallations,
createInstallation, createInstallation,

View File

@ -10,12 +10,13 @@ export const LogContext = createContext<LogContextProviderProps | undefined>(
undefined undefined
); );
// Create a UserContextProvider component
export const LogContextProvider = ({ children }: { children: ReactNode }) => { export const LogContextProvider = ({ children }: { children: ReactNode }) => {
const [installationStatus, setInstallationStatus] = useState< const [installationStatus, setInstallationStatus] = useState<
Record<number, number[]> Record<number, number[]>
>({}); >({});
const BUFFER_LENGTH = 5;
const handleLogWarningOrError = (installation_id: number, value: number) => { const handleLogWarningOrError = (installation_id: number, value: number) => {
setInstallationStatus((prevStatus) => { setInstallationStatus((prevStatus) => {
const newStatus = { ...prevStatus }; const newStatus = { ...prevStatus };
@ -24,16 +25,23 @@ export const LogContextProvider = ({ children }: { children: ReactNode }) => {
newStatus[installation_id] = []; newStatus[installation_id] = [];
} }
newStatus[installation_id].unshift(value); newStatus[installation_id].unshift(value);
newStatus[installation_id] = newStatus[installation_id].slice(0, 5); newStatus[installation_id] = newStatus[installation_id].slice(
0,
BUFFER_LENGTH
);
return newStatus; return newStatus;
}); });
}; };
const getStatus = (installationId: number) => { const getStatus = (installationId: number) => {
let status; let status;
if (!installationStatus.hasOwnProperty(installationId)) { if (!installationStatus.hasOwnProperty(installationId)) {
status = -2; status = -2;
} else { } else {
if (installationId === 2) {
console.log(installationStatus[2]);
}
if (installationStatus[installationId][0] == -1) { if (installationStatus[installationId][0] == -1) {
let i = 0; let i = 0;
for (i; i < installationStatus[installationId].length; i++) { for (i; i < installationStatus[installationId].length; i++) {

View File

@ -8,6 +8,7 @@ import {RecordSeries} from './data';
import {isUndefined, Maybe} from './utils/maybe'; import {isUndefined, Maybe} from './utils/maybe';
import {isNumber} from './utils/runtimeTypeChecking'; import {isNumber} from './utils/runtimeTypeChecking';
import {I_CsvEntry} from 'src/content/dashboards/Log/graph.util'; import {I_CsvEntry} from 'src/content/dashboards/Log/graph.util';
import {I_S3Credentials} from '../interfaces/S3Types';
export const FetchResult = { export const FetchResult = {
notAvailable: 'N/A', notAvailable: 'N/A',
@ -33,7 +34,10 @@ function reverseBits(x: number): number {
} }
export default class DataCache<T extends Record<string, I_CsvEntry>> { export default class DataCache<T extends Record<string, I_CsvEntry>> {
readonly _fetch: (t: UnixTime) => Promise<FetchResult<T>>; readonly _fetch: (
t: UnixTime,
s3Credentials: I_S3Credentials
) => Promise<FetchResult<T>>;
public readonly gotData: Observable<UnixTime>; public readonly gotData: Observable<UnixTime>;
@ -41,16 +45,23 @@ export default class DataCache<T extends Record<string, I_CsvEntry>> {
private readonly resolution: TimeSpan; private readonly resolution: TimeSpan;
private readonly s3Credentials: I_S3Credentials;
private readonly fetchQueue = createDispatchQueue(6); private readonly fetchQueue = createDispatchQueue(6);
private readonly fetching: Set<number> = new Set<number>(); private readonly fetching: Set<number> = new Set<number>();
constructor( constructor(
fetch: (t: UnixTime) => Promise<FetchResult<T>>, fetch: (
resolution: TimeSpan t: UnixTime,
s3Credentials: I_S3Credentials
) => Promise<FetchResult<T>>,
resolution: TimeSpan,
s3Credentials: I_S3Credentials
) { ) {
this._fetch = fetch; this._fetch = fetch;
this.resolution = resolution; this.resolution = resolution;
this.s3Credentials = s3Credentials;
this.gotData = new Subject<UnixTime>(); this.gotData = new Subject<UnixTime>();
} }
@ -71,7 +82,7 @@ export default class DataCache<T extends Record<string, I_CsvEntry>> {
const t = time.ticks; const t = time.ticks;
const node = this.cache.find(t); const node = this.cache.find(t);
if (node.index !== t) this.fetchData(time); if (node.index !== t) this.fetchData(time, this.s3Credentials);
} }
} }
@ -82,7 +93,7 @@ export default class DataCache<T extends Record<string, I_CsvEntry>> {
const node = this.cache.find(t); const node = this.cache.find(t);
if (node.index === t) return node.value; if (node.index === t) return node.value;
if (fetch) this.fetchData(time); if (fetch) this.fetchData(time, this.s3Credentials);
return this.interpolate(node, t); return this.interpolate(node, t);
} }
@ -130,7 +141,7 @@ export default class DataCache<T extends Record<string, I_CsvEntry>> {
return interpolated as T; return interpolated as T;
} }
private fetchData(time: UnixTime) { private fetchData(time: UnixTime, s3Credentials: I_S3Credentials) {
const t = time.ticks; const t = time.ticks;
if (this.fetching.has(t)) if (this.fetching.has(t))
@ -160,7 +171,7 @@ export default class DataCache<T extends Record<string, I_CsvEntry>> {
(this.gotData as Subject<UnixTime>).next(time); (this.gotData as Subject<UnixTime>).next(time);
}; };
return this._fetch(time) return this._fetch(time, s3Credentials)
.then( .then(
(d) => onSuccess(d), (d) => onSuccess(d),
(f) => onFailure(f) (f) => onFailure(f)

View File

@ -7,6 +7,8 @@ export interface I_S3Credentials {
} }
export interface Notification { export interface Notification {
key: string; device: string;
value: string; description: string;
date: string;
time: string;
} }

View File

@ -16,7 +16,6 @@
"german": "Deutsch", "german": "Deutsch",
"groupTabs": "Gruppen", "groupTabs": "Gruppen",
"groupTree": "Gruppenbaum", "groupTree": "Gruppenbaum",
"information": "Information",
"inheritedAccess": "Vererbter Zugriff von", "inheritedAccess": "Vererbter Zugriff von",
"installation": "Installation", "installation": "Installation",
"installationTabs": "Installationen", "installationTabs": "Installationen",
@ -48,5 +47,24 @@
"live": "Live Übertragung", "live": "Live Übertragung",
"deleteInstallation": "Installation löschen", "deleteInstallation": "Installation löschen",
"errorOccured": "Ein Fehler ist aufgetreten", "errorOccured": "Ein Fehler ist aufgetreten",
"successfullyUpdated": "Erfolgreich aktualisiert" "successfullyUpdated": "Erfolgreich aktualisiert",
"grantAccess": "Zugriff gewähren",
"UserswithDirectAccess": "Benutzer mit direktem Zugriff",
"UserswithInheritedAccess": "Benutzer mit geerbtem Zugriff",
"noerrors": "Es liegen keine Fehler vor",
"nowarnings": "Es gibt keine Warnungen",
"noUsersWithDirectAccessToThis": "Keine Benutzer mit direktem Zugriff darauf ",
"selectUsers": "Wählen Sie Benutzer aus",
"cancel": "Stornieren",
"addNewFolder": "Neuen Ordner hinzufügen",
"addNewInstallation": "Neue Installation hinzufügen",
"deleteFolder": "Lösche Ordner",
"grantAccessToFolders": "Gewähren Sie Zugriff auf Ordner",
"grantAccessToInstallations": "Gewähren Sie Zugriff auf Installationen",
"cannotloadloggingdata": "Protokollierungsdaten können nicht geladen werden",
"grantedAccessToUsers": "Benutzern Zugriff gewährt: ",
"unableToGrantAccess": "Der Zugriff auf kann nicht gewährt werden: ",
"unableToLoadData": "Daten können nicht geladen werden",
"unableToRevokeAccess": "Der Zugriff kann nicht widerrufen werden",
"revokedAccessFromUser": "Zugriff vom Benutzer widerrufen: "
} }

View File

@ -1,8 +1,6 @@
{ {
"liveView": "Live view",
"allInstallations": "All installations", "allInstallations": "All installations",
"applyChanges": "Apply changes", "applyChanges": "Apply changes",
"deleteInstallation": "Delete Installation",
"country": "Country", "country": "Country",
"customerName": "Customer name", "customerName": "Customer name",
"english": "English", "english": "English",
@ -53,5 +51,24 @@
"live": "Live View", "live": "Live View",
"deleteInstallation": "Delete Installation", "deleteInstallation": "Delete Installation",
"errorOccured": "An error has occurred", "errorOccured": "An error has occurred",
"successfullyUpdated": "Successfully updated" "successfullyUpdated": "Successfully updated",
"grantAccess": "Grant Access",
"UserswithDirectAccess": "Users with Direct Access",
"UserswithInheritedAccess": "Users with Inherited Access",
"noerrors": "There are no errors",
"nowarnings": "There are no warnings",
"noUsersWithDirectAccessToThis": "No users with direct access to this ",
"selectUsers": "Select Users",
"cancel": "Cancel",
"addNewFolder": "Add new Folder",
"addNewInstallation": "Add new Installation",
"deleteFolder": "Delete Folder",
"grantAccessToFolders": "Grant Access to Folders",
"grantAccessToInstallations": "Grant Access to Installations",
"cannotloadloggingdata": "Cannot load logging data",
"grantedAccessToUsers": "Granted access to users: ",
"unableToGrantAccess": "Unable to grant access to: ",
"unableToLoadData": "Unable to load data",
"unableToRevokeAccess": "Unable to revoke access",
"revokedAccessFromUser": "Revoked access from user: "
} }

View File

@ -16,7 +16,6 @@
"german": "Allemand", "german": "Allemand",
"groupTabs": "Onglets de groupe", "groupTabs": "Onglets de groupe",
"groupTree": "Arbre de groupe", "groupTree": "Arbre de groupe",
"information": "Informations",
"inheritedAccess": "Accès hérité de", "inheritedAccess": "Accès hérité de",
"installation": "Installation", "installation": "Installation",
"installationTabs": "Onglets d'installation", "installationTabs": "Onglets d'installation",
@ -48,5 +47,24 @@
"live": "Diffusion en direct", "live": "Diffusion en direct",
"deleteInstallation": "Supprimer l'installation", "deleteInstallation": "Supprimer l'installation",
"errorOccured": "Une erreur est survenue", "errorOccured": "Une erreur est survenue",
"successfullyUpdated": "Mise à jour réussie" "successfullyUpdated": "Mise à jour réussie",
"grantAccess": "Accorder l'accès",
"UserswithDirectAccess": "Utilisateurs avec accès direct",
"UserswithInheritedAccess": "Utilisateurs avec accès hérité",
"noerrors": "Il n'y a pas d'erreurs",
"nowarnings": "Il n'y a aucun avertissement",
"noUsersWithDirectAccessToThis": "Aucun utilisateur ayant un accès direct à ceci ",
"selectUsers": "Sélectionnez les utilisateurs",
"cancel": "Annuler",
"addNewFolder": "Ajouter un nouveau dossier",
"addNewInstallation": "Ajouter une nouvelle installation",
"deleteFolder": "Supprimer le dossier",
"grantAccessToFolders": "Accorder l'accès aux dossiers",
"grantAccessToInstallations": "Accorder l'accès aux installations",
"cannotloadloggingdata": "Impossible de charger les données de journalisation",
"grantedAccessToUsers": "Accès accordé aux utilisateurs: ",
"unableToGrantAccess": "Impossible d'accorder l'accès à: ",
"unableToLoadData": "Impossible de charger les données",
"unableToRevokeAccess": "Impossible de révoquer l'accès",
"revokedAccessFromUser": "Accès révoqué de l'utilisateur: "
} }

View File

@ -4,8 +4,7 @@ import {
ListItem, ListItem,
ListItemText, ListItemText,
Menu, Menu,
MenuItem, MenuItem
Switch
} from '@mui/material'; } from '@mui/material';
import { useContext, useRef, useState } from 'react'; import { useContext, useRef, useState } from 'react';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
@ -98,6 +97,11 @@ function HeaderMenu(props: HeaderButtonsProps) {
setOpen(false); setOpen(false);
}; };
const handleLanguageSelect = (language) => {
props.onSelectLanguage(language);
handleClose();
};
return ( return (
<> <>
<ListWrapper <ListWrapper
@ -129,15 +133,14 @@ function HeaderMenu(props: HeaderButtonsProps) {
</ListItem> </ListItem>
</List> </List>
</ListWrapper> </ListWrapper>
<Switch checked={darkState} onChange={handleThemeChange} />
<Menu anchorEl={ref.current} onClose={handleClose} open={isOpen}> <Menu anchorEl={ref.current} onClose={handleClose} open={isOpen}>
<MenuItem value="en" onClick={() => props.onSelectLanguage('en')}> <MenuItem value="en" onClick={() => handleLanguageSelect('en')}>
English English
</MenuItem> </MenuItem>
<MenuItem value="de" onClick={() => props.onSelectLanguage('de')}> <MenuItem value="de" onClick={() => handleLanguageSelect('de')}>
German German
</MenuItem> </MenuItem>
<MenuItem value="fr" onClick={() => props.onSelectLanguage('fr')}> <MenuItem value="fr" onClick={() => handleLanguageSelect('fr')}>
French French
</MenuItem> </MenuItem>
</Menu> </Menu>

View File

@ -18,7 +18,7 @@ import SidebarMenu from './SidebarMenu';
const SidebarWrapper = styled(Box)( const SidebarWrapper = styled(Box)(
({ theme }) => ` ({ theme }) => `
width: ${theme.sidebar.width}; width: ${theme.sidebar.width};
min-width: ${theme.sidebar.width}; min-width: "${theme.sidebar.width}";
color: ${theme.colors.alpha.trueWhite[70]}; color: ${theme.colors.alpha.trueWhite[70]};
position: relative; position: relative;
z-index: 7; z-index: 7;
@ -64,8 +64,7 @@ function Sidebar() {
alt="innovenergy logo" alt="innovenergy logo"
style={{ style={{
maxWidth: '150px', // Maximum width for the image maxWidth: '150px', // Maximum width for the image
maxHeight: '150px', // Maximum height for the image maxHeight: '150px' // Maximum height for the image
marginLeft: '50px' // Adjust the value as needed
}} }}
/> />
</Box> </Box>

View File

@ -1,4 +1,5 @@
import { Box, styled } from '@mui/material'; import { Box, Card, styled } from '@mui/material';
import Avatar from '@mui/material/Avatar';
export const TabsContainerWrapper = styled(Box)( export const TabsContainerWrapper = styled(Box)(
({ theme }) => ` ({ theme }) => `
@ -81,3 +82,61 @@ export const TabsContainerWrapper = styled(Box)(
} }
` `
); );
export const AvatarWrapper = styled(Avatar)(
({ theme }) => `
display: flex;
align-items: center;
justify-content: center;
border-radius: 60px;
margin-top: -5px;
height: ${theme.spacing(3.5)};
width: ${theme.spacing(3.5)};
background: ${
theme.palette.mode === 'dark'
? theme.colors.alpha.trueWhite[30]
: '#ffffff'
};
img {
background: ${theme.colors.alpha.trueWhite[100]};
display: block;
border-radius: inherit;
height: ${theme.spacing(4.5)};
width: ${theme.spacing(4.5)};
}
`
);
export const AvatarAddWrapper = styled(Avatar)(
({ theme }) => `
background: ${theme.colors.alpha.black[10]};
color: ${theme.colors.primary.main};
width: ${theme.spacing(8)};
height: ${theme.spacing(8)};
`
);
export const CardAddAction = styled(Card)(
({ theme }) => `
border: ${theme.colors.primary.main} dashed 1px;
height: 100%;
color: ${theme.colors.primary.main};
transition: ${theme.transitions.create(['all'])};
.MuiCardActionArea-root {
height: 100%;
justify-content: center;
align-items: center;
display: flex;
}
.MuiTouchRipple-root {
opacity: .2;
}
&:hover {
border-color: ${theme.colors.alpha.black[70]};
}
`
);

View File

@ -240,7 +240,7 @@ export const PureLightTheme = createTheme({
menuItemHeadingColor: colors.layout.sidebar.menuItemHeadingColor, menuItemHeadingColor: colors.layout.sidebar.menuItemHeadingColor,
boxShadow: boxShadow:
'2px 0 3px rgba(159, 162, 191, .18), 1px 0 1px rgba(159, 162, 191, 0.32)', '2px 0 3px rgba(159, 162, 191, .18), 1px 0 1px rgba(159, 162, 191, 0.32)',
width: '290px' width: '200px'
}, },
header: { header: {
height: '80px', height: '80px',