updated frontend

This commit is contained in:
Noe 2023-10-09 14:56:12 +02:00
parent 651f2974c4
commit 88b79dd398
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",
"apexcharts": "3.35.3",
"axios": "^1.5.0",
"chart.js": "^4.4.0",
"clsx": "1.1.1",
"cytoscape": "^3.26.0",
"date-fns": "2.28.0",
"history": "5.3.0",
"linq-to-typescript": "^11.0.0",
@ -23,9 +25,14 @@
"prop-types": "15.8.1",
"react": "17.0.2",
"react-apexcharts": "1.4.0",
"react-chartjs-2": "^5.2.0",
"react-custom-scrollbars-2": "4.4.0",
"react-cytoscapejs": "^2.0.0",
"react-dom": "17.0.2",
"react-flow-renderer": "^10.3.17",
"react-helmet-async": "1.3.0",
"react-icons": "^4.11.0",
"react-icons-converter": "^1.1.4",
"react-intl": "^6.4.4",
"react-router": "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 ResetPassword from './components/ResetPassword';
import ForgotPassword from './components/ForgotPassword';
import InstallationTabs from './content/dashboards/Installations/index';
import routes from 'src/Resources/routes.json';
import './App.css';
function App() {
//const content = useRoutes(router);
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const tokencontext = useContext(TokenContext);
const { token, setNewToken, removeToken } = tokencontext;
const [forgotPassword, setForgotPassword] = useState(false);
@ -79,14 +79,14 @@ function App() {
lazy(() => import('src/content/pages/Status/Maintenance'))
);
const routes: RouteObject[] = [
const routesArray: RouteObject[] = [
{
path: '',
element: <BaseLayout />,
children: [
{
path: '/',
element: <Navigate to="installations" replace />
element: <Navigate to="installations/" replace />
},
{
path: 'status',
@ -118,39 +118,6 @@ function App() {
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) {
@ -189,7 +156,7 @@ function App() {
>
<CssBaseline />
<Routes>
{routes.map((route, index) => (
{routesArray.map((route, index) => (
<Route key={index} path={route.path} element={route.element}>
{route.children &&
route.children.map((childRoute, childIndex) => (
@ -201,6 +168,23 @@ function App() {
))}
</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>
</IntlProvider>
</ThemeProvider>

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import {
FormControl,
Grid,
@ -9,32 +9,42 @@ import {
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
import FlatInstallationView from 'src/content/dashboards/Installations/FlatInstallationView';
import LogContextProvider from 'src/contexts/LogContextProvider';
function InstallationSearch() {
const theme = useTheme();
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(() => {
fetchAllInstallations();
}, []);
const [filteredData, setFilteredData] = useState(data);
const [filteredData, setFilteredData] = useState(installations);
useEffect(() => {
const filtered = data.filter(
const filtered = installations.filter(
(item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.location.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredData(filtered);
}, [searchTerm, data]);
}, [searchTerm, installations]);
return (
<>
<Grid container spacing={4}>
<Grid item xs={12} md={3}>
<FormControl variant="outlined" fullWidth>
<Grid container>
<Grid
item
xs={12}
md={4}
sx={{ display: !installationId ? 'block' : 'none' }}
>
<FormControl variant="outlined">
<TextField
placeholder="Search"
value={searchTerm}
@ -51,7 +61,9 @@ function InstallationSearch() {
</FormControl>
</Grid>
</Grid>
<FlatInstallationView installations={filteredData} />
<LogContextProvider>
<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 { Box, Card, Container, Grid, Tab, Tabs, useTheme } from '@mui/material';
import InstallationsContextProvider from 'src/contexts/InstallationsContextProvider';
import InstallationSearch from './InstallationSearch';
import { Card, Container, Grid, Tab, Tabs, useTheme } from '@mui/material';
import ListIcon from '@mui/icons-material/List';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import InstallationTree from '../Tree/InstallationTree';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
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() {
const theme = useTheme();
const [currentTab, setCurrentTab] = useState<string>('flat');
const location = useLocation();
const navigate = useNavigate();
const tabs = [
{
value: 'flat',
value: 'list',
label: 'Flat view',
icon: <ListIcon id="mode-toggle-button-list-icon" />,
component: {}
icon: <ListIcon id="mode-toggle-button-list-icon" />
},
{
value: 'tree',
@ -27,9 +32,25 @@ function InstallationTabs() {
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 => {
setCurrentTab(value);
navigate(value);
};
return (
@ -45,40 +66,30 @@ function InstallationTabs() {
indicatorColor="primary"
>
{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>
</TabsContainerWrapper>
<InstallationsContextProvider>
<LogContextProvider>
<Card variant="outlined">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={0}
>
{currentTab === 'tree' && (
<>
<Grid item xs={12}>
<Box p={4}>
<InstallationTree />
</Box>
</Grid>
</>
)}
{currentTab === 'flat' && (
<Grid item xs={12}>
<Box p={4}>
<InstallationSearch />
</Box>
</Grid>
)}
</Grid>
</Card>
</LogContextProvider>
</InstallationsContextProvider>
<Card variant="outlined">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={0}
>
<Routes>
<Route path={routes.list + '*'} element={<FlatView />} />
<Route path={routes.tree + '*'} element={<TreeView />} />
</Routes>
</Grid>
</Card>
</Container>
<Footer />
</UsersContextProvider>

View File

@ -13,6 +13,7 @@ import { Close as CloseIcon } from '@mui/icons-material';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import { TokenContext } from 'src/contexts/tokenContext';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import { FormattedMessage } from 'react-intl';
interface installationFormProps {
cancel: () => void;
@ -96,7 +97,12 @@ function installationForm(props: installationFormProps) {
>
<div>
<TextField
label="Customer Name"
label={
<FormattedMessage
id="customerName"
defaultMessage="Customer Name"
/>
}
name="name"
value={formValues.name}
onChange={handleChange}
@ -107,7 +113,7 @@ function installationForm(props: installationFormProps) {
</div>
<div>
<TextField
label="Region"
label={<FormattedMessage id="region" defaultMessage="Region" />}
name="region"
value={formValues.region}
onChange={handleChange}
@ -118,7 +124,9 @@ function installationForm(props: installationFormProps) {
</div>
<div>
<TextField
label="Location"
label={
<FormattedMessage id="location" defaultMessage="Location" />
}
name="location"
value={formValues.location}
onChange={handleChange}
@ -130,7 +138,9 @@ function installationForm(props: installationFormProps) {
<div>
<TextField
label="Country"
label={
<FormattedMessage id="country" defaultMessage="Country" />
}
name="country"
value={formValues.country}
onChange={handleChange}
@ -141,7 +151,12 @@ function installationForm(props: installationFormProps) {
</div>
<div>
<TextField
label="Order Numbers"
label={
<FormattedMessage
id="orderNumbers"
defaultMessage="Order Numbers"
/>
}
name="orderNumbers"
value={formValues.orderNumbers}
onChange={handleChange}
@ -164,7 +179,7 @@ function installationForm(props: installationFormProps) {
}}
disabled={!areRequiredFieldsFilled()}
>
Submit
<FormattedMessage id="submit" defaultMessage="Submit" />
</Button>
<Button
@ -174,7 +189,7 @@ function installationForm(props: installationFormProps) {
marginLeft: '10px'
}}
>
Cancel
<FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button>
{loading && (
@ -195,11 +210,14 @@ function installationForm(props: installationFormProps) {
alignItems: 'center'
}}
>
An error has occurred
<FormattedMessage
id="errorOccured"
defaultMessage="An error has occured"
/>
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)} // Set error state to false on click
onClick={() => setError(false)}
sx={{ marginLeft: '4px' }}
>
<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 {
Alert,
Card,
Container,
Divider,
Grid,
IconButton,
ListItem,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
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';
import { FormattedMessage } from 'react-intl';
import ErrorIcon from '@mui/icons-material/Error';
interface LogProps {
warnings: Notification[];
@ -29,43 +33,171 @@ function Log(props: LogProps) {
<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
}}
{(props.errors.length > 0 || props.warnings.length > 0) && (
<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"
/>
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="body1" sx={{ fontWeight: 'bold' }}>
{'Error from: ' +
error.key +
' device: ' +
error.value}
</Typography>
}
/>
</ListItem>
<Divider />
</Fragment>
);
})}
</TableCell>
<TableCell>
<FormattedMessage id="date" defaultMessage="Date" />
</TableCell>
<TableCell>
<FormattedMessage id="time" defaultMessage="Time" />
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.errors.map((error, index) => {
return (
<TableRow hover key={index}>
<TableCell>
<ErrorIcon
sx={{
color: 'red',
width: 25,
height: 25,
marginLeft: '5px',
marginTop: '8px'
}}
/>
</TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px' }}
>
{error.device}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body1"
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 && (
<Alert
severity="error"
@ -77,7 +209,10 @@ function Log(props: LogProps) {
marginBottom: '20px'
}}
>
There are no errors
<FormattedMessage
id="noerrors"
defaultMessage="There are no errors"
/>
<IconButton
color="inherit"
size="small"
@ -86,19 +221,7 @@ function Log(props: LogProps) {
</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
@ -108,10 +231,12 @@ function Log(props: LogProps) {
display: 'flex',
alignItems: 'center',
marginTop: '20px'
//marginBottom: '20px'
}}
>
Cannot load logging data
<FormattedMessage
id="cannotloadloggingdata"
defaultMessage="Cannot load logging data"
/>
<IconButton
color="inherit"
size="small"
@ -119,43 +244,6 @@ function Log(props: LogProps) {
></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
@ -163,11 +251,14 @@ function Log(props: LogProps) {
sx={{
ml: 1,
display: 'flex',
alignItems: 'center',
marginBottom: '20px'
alignItems: 'center'
//marginBottom: '20px'
}}
>
There are no warnings
<FormattedMessage
id="nowarnings"
defaultMessage="There are no warnings"
/>
<IconButton
color="inherit"
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 {
value: string | number;
@ -22,3 +23,173 @@ export const parseCsv = (text: string): 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 { UserContext } from 'src/contexts/userContext';
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 Avatar from '@mui/material/Avatar';
import ListItemText from '@mui/material/ListItemText';
@ -26,7 +26,8 @@ import PersonIcon from '@mui/icons-material/Person';
import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material';
import { UsersContext } from 'src/contexts/UsersContextProvider';
import { AccessContext } from '../../../contexts/AccessContextProvider';
import { AccessContext } from 'src/contexts/AccessContextProvider';
import { FormattedMessage } from 'react-intl';
interface AccessProps {
currentResource: I_Folder | I_Installation;
@ -157,15 +158,32 @@ function Access(props: AccessProps) {
if (NotGrantedAccessUsers.length > 0) {
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) {
setUpdatedMessage(
'Granted access to users: ' + grantedAccessUsers.join(', ')
);
const message =
(
<FormattedMessage
id="grantedAccessToUsers"
defaultMessage="Granted access to users: "
/>
).props.defaultMessage +
' ' +
grantedAccessUsers.join(', ');
setUpdatedMessage(message);
setUpdated(true);
setTimeout(() => {
@ -248,7 +266,10 @@ function Access(props: AccessProps) {
//backgroundColor: 'white'
}}
>
Select users
<FormattedMessage
id="selectUsers"
defaultMessage="Select Users"
/>
</InputLabel>
<Select
multiple
@ -301,7 +322,7 @@ function Access(props: AccessProps) {
}}
onClick={handleSubmit}
>
Submit
<FormattedMessage id="submit" defaultMessage="Submit" />
</Button>
<Button
variant="contained"
@ -317,7 +338,7 @@ function Access(props: AccessProps) {
padding: '6px 8px'
}}
>
Cancel
<FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button>
</Box>
</Box>
@ -333,7 +354,7 @@ function Access(props: AccessProps) {
'&:hover': { bgcolor: '#f7b34d' }
}}
>
Grant Access
<FormattedMessage id="grantAccess" defaultMessage="Grant Access" />
</Button>
</Grid>
<Grid item xs={12} md={12}>
@ -342,7 +363,10 @@ function Access(props: AccessProps) {
onClick={handleDirectButtonPressed}
sx={{ marginTop: '20px' }}
>
Users with Direct Access
<FormattedMessage
id="UserswithDirectAccess"
defaultMessage="Users with Direct Access"
/>
</Button>
{directButtonPressed &&
@ -382,8 +406,11 @@ function Access(props: AccessProps) {
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
color="inherit"
size="small"
@ -398,7 +425,10 @@ function Access(props: AccessProps) {
onClick={handleInheritedButtonPressed}
sx={{ marginTop: '20px', marginBottom: '20px' }}
>
Users with Inherited Access
<FormattedMessage
id="UserswithInheritedAccess"
defaultMessage="Users with Inherited Access"
/>
</Button>
{inheritedButtonPressed && usersWithInheritedAccess.length == 0 && (
<Alert
@ -410,7 +440,10 @@ function Access(props: AccessProps) {
marginBottom: '20px'
}}
>
There are no users with inherited access to this{' '}
<FormattedMessage
id="noUsersWithDirectAccessToThis"
defaultMessage="No users with direct access to this "
/>
{props.resourceType}.
<IconButton
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 { TreeItem } from '@mui/lab';
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 { makeStyles } from '@mui/styles';
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 {
node: I_Installation | I_Folder;
parent_id: number;
children?: ReactNode;
handleSelectedInstallation: () => void;
status: number;
}
const useTreeItemStyles = makeStyles((theme) => ({
@ -37,6 +38,41 @@ const useTreeItemStyles = makeStyles((theme) => ({
function CustomTreeItem(props: CustomTreeItemProps) {
const theme = useTheme();
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 = () => {
if (props.node.type === 'Folder') {
return <FolderIcon />;
@ -45,13 +81,14 @@ function CustomTreeItem(props: CustomTreeItemProps) {
}
return null;
};
const itemClasses = [classes.labelRoot];
return (
<TreeItem
nodeId={
props.node.id.toString() + props.parent_id.toString() + props.node.type
}
nodeId={props.node.id.toString() + props.node.type}
label={
<div className={classes.labelRoot}>
<div className={itemClasses.join(' ')}>
<ListItemIcon color="inherit" className={classes.labelIcon}>
{renderIcon()}
</ListItemIcon>
@ -63,16 +100,17 @@ function CustomTreeItem(props: CustomTreeItemProps) {
>
{props.node.name}
</Typography>
{props.node.type === 'Installation' && (
<div>
{props.status === -1 ? (
{status === -1 ? (
<CancelIcon
style={{
width: '23px',
height: '23px',
color: 'red',
borderRadius: '50%',
marginRight: '40px',
marginLeft: '21px',
marginTop: '30px'
}}
/>
@ -80,12 +118,12 @@ function CustomTreeItem(props: CustomTreeItemProps) {
''
)}
{props.status === -2 ? (
{status === -2 ? (
<CircularProgress
size={20}
sx={{
color: '#f7b34d',
marginRight: '40px',
marginLeft: '20px',
marginTop: '30px'
}}
/>
@ -98,13 +136,13 @@ function CustomTreeItem(props: CustomTreeItemProps) {
width: '20px',
height: '20px',
borderRadius: '50%',
marginRight: '40px',
marginLeft: '20px',
backgroundColor:
props.status === 2
status === 2
? 'red'
: props.status === 1
: status === 1
? 'orange'
: props.status === -1 || props.status === -2
: status === -1 || status === -2
? 'transparent'
: 'green'
}}
@ -114,6 +152,7 @@ function CustomTreeItem(props: CustomTreeItemProps) {
</div>
}
sx={{
display: !installationId ? 'block' : 'none',
'.MuiTreeItem-content': {
width: 'inherit',
@ -123,9 +162,10 @@ function CustomTreeItem(props: CustomTreeItemProps) {
cursor: 'pointer',
backgroundColor: theme.colors.primary.lighter
}
}
},
backgroundColor: selected ? '#111111' : '#ffffff'
}}
onClick={() => props.handleSelectedInstallation()}
onClick={() => handleSelectOneInstallation()}
>
{props.children}
</TreeItem>

View File

@ -24,6 +24,7 @@ import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import AccessContextProvider from 'src/contexts/AccessContextProvider';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import Access from '../ManageAccess/Access';
import { FormattedMessage } from 'react-intl';
interface singleFolderProps {
current_folder: I_Folder;
@ -43,6 +44,8 @@ function Folder(props: singleFolderProps) {
const [isRowHovered, setHoveredRow] = useState(-1);
const [selectedUser, setSelectedUser] = useState<number>(-1);
const selectedBulkActions = selectedUser !== -1;
const searchParams = new URLSearchParams(location.search);
const folderId = parseInt(searchParams.get('folder'));
const installationContext = useContext(InstallationsContext);
const {
@ -65,8 +68,16 @@ function Folder(props: singleFolderProps) {
}
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 => {
@ -140,210 +151,243 @@ function Folder(props: singleFolderProps) {
}
return true;
};
return (
<>
{openModalFolder && (
<FolderForm
cancel={handleFormCancel}
submit={handleFolderFormSubmit}
parentid={props.current_folder.id}
/>
)}
{openModalInstallation && (
<InstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}
parentid={props.current_folder.id}
/>
)}
<Grid item xs={12} md={9}>
<TabsContainerWrapper>
<Tabs
onChange={handleTabsChange}
value={currentTab}
variant="scrollable"
scrollButtons="auto"
textColor="primary"
indicatorColor="primary"
>
{tabs.map((tab) => (
<Tab key={tab.value} label={tab.label} value={tab.value} />
))}
</Tabs>
</TabsContainerWrapper>
<Card variant="outlined">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={0}
>
{currentTab === 'folder' && (
<Container maxWidth="xl">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12} md={12}>
<CardContent>
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '50ch' }
}}
noValidate
autoComplete="off"
>
<div>
<TextField
label="Name"
name="name"
value={formValues.name}
onChange={handleChange}
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label="Information"
name="information"
value={formValues.information}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
if (folderId == props.current_folder.id) {
return (
<>
{openModalFolder && (
<FolderForm
cancel={handleFormCancel}
submit={handleFolderFormSubmit}
parentid={props.current_folder.id}
/>
)}
{openModalInstallation && (
<InstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}
parentid={props.current_folder.id}
/>
)}
<Grid item xs={12} md={9}>
<TabsContainerWrapper>
<Tabs
onChange={handleTabsChange}
value={currentTab}
variant="scrollable"
scrollButtons="auto"
textColor="primary"
indicatorColor="primary"
>
{tabs.map((tab) => (
<Tab key={tab.value} label={tab.label} value={tab.value} />
))}
</Tabs>
</TabsContainerWrapper>
<Card variant="outlined">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={0}
>
{currentTab === 'folder' && (
<Container maxWidth="xl">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12} md={12}>
<CardContent>
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '50ch' }
}}
noValidate
autoComplete="off"
>
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleFolderInformationUpdate}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
Apply Changes
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleNewInstallationInsertion}
sx={{
marginLeft: '10px'
}}
>
Add new installation
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleNewFolderInsertion}
sx={{
marginLeft: '10px'
}}
>
Add new folder
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleDeleteFolder}
sx={{
marginLeft: '10px'
}}
>
Delete Folder
</Button>
)}
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
<div>
<TextField
label={
<FormattedMessage
id="name"
defaultMessage="Name"
/>
}
name="name"
value={formValues.name}
onChange={handleChange}
fullWidth
required
error={formValues.name === ''}
/>
)}
{error && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
An error has occurred
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)} // Set error state to false on click
sx={{ marginLeft: '4px' }}
</div>
<div>
<TextField
label={
<FormattedMessage
id="information"
defaultMessage="Information"
/>
}
name="information"
value={formValues.information}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleFolderInformationUpdate}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
{updated && (
<Alert
severity="success"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
Successfully updated
<IconButton
color="inherit"
size="small"
onClick={() => setUpdated(false)} // Set error state to false on click
sx={{ marginLeft: '4px' }}
<FormattedMessage
id="applyChanges"
defaultMessage="Apply Changes"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleNewInstallationInsertion}
sx={{
marginLeft: '10px'
}}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
</div>
</Box>
</CardContent>
<FormattedMessage
id="addNewInstallation"
defaultMessage="Add new installation"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleNewFolderInsertion}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="addNewFolder"
defaultMessage="Add new folder"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleDeleteFolder}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="deleteFolder"
defaultMessage="Delete Folder"
/>
</Button>
)}
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
/>
)}
{error && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
<FormattedMessage
id="errorOccured"
defaultMessage="An error has occurred"
/>
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)} // Set error state to false on click
sx={{ marginLeft: '4px' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
{updated && (
<Alert
severity="success"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
<FormattedMessage
id="successfullyUpdated"
defaultMessage="Successfully updated"
/>
<IconButton
color="inherit"
size="small"
onClick={() => setUpdated(false)} // Set error state to false on click
sx={{ marginLeft: '4px' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
</div>
</Box>
</CardContent>
</Grid>
</Grid>
</Grid>
</Container>
)}
{currentTab === 'manage' && currentUser.hasWriteAccess && (
<AccessContextProvider>
<Access
currentResource={formValues}
resourceType="folder"
></Access>
</AccessContextProvider>
)}
</Grid>
</Card>
</Grid>
</>
);
</Container>
)}
{currentTab === 'manage' && currentUser.hasWriteAccess && (
<AccessContextProvider>
<Access
currentResource={formValues}
resourceType="folder"
></Access>
</AccessContextProvider>
)}
</Grid>
</Card>
</Grid>
</>
);
} else {
return null;
}
}
export default Folder;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import {
import React, {
createContext,
ReactNode,
useCallback,
@ -11,6 +11,7 @@ import {
I_UserWithInheritedAccess,
InnovEnergyUser
} from '../interfaces/UserTypes';
import { FormattedMessage } from 'react-intl';
interface AccessContextProviderProps {
usersWithDirectAccess: InnovEnergyUser[];
@ -82,7 +83,15 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
})
.catch((error) => {
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) => {
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,
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);
setTimeout(() => {
setUpdated(false);
@ -136,7 +163,14 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
})
.catch((error) => {
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';
interface I_InstallationContextProviderProps {
data: I_Installation[];
installations: I_Installation[];
foldersAndInstallations: I_Installation[];
fetchAllInstallations: () => Promise<void>;
fetchAllFoldersAndInstallations: () => Promise<void>;
createInstallation: (value: Partial<I_Installation>) => Promise<void>;
@ -24,14 +25,14 @@ interface I_InstallationContextProviderProps {
setUpdated: (value: boolean) => void;
deleteInstallation: (value: I_Installation, view: string) => Promise<void>;
createFolder: (value: Partial<I_Folder>) => Promise<void>;
updateFolder: (value: I_Folder) => Promise<void>;
deleteFolder: (value: I_Folder) => Promise<void>;
}
export const InstallationsContext =
createContext<I_InstallationContextProviderProps>({
data: [],
installations: [],
foldersAndInstallations: [],
fetchAllInstallations: () => Promise.resolve(),
fetchAllFoldersAndInstallations: () => Promise.resolve(),
createInstallation: () => Promise.resolve(),
@ -44,7 +45,6 @@ export const InstallationsContext =
setUpdated: () => {},
deleteInstallation: () => Promise.resolve(),
createFolder: () => Promise.resolve(),
updateFolder: () => Promise.resolve(),
deleteFolder: () => Promise.resolve()
});
@ -54,7 +54,8 @@ const InstallationsContextProvider = ({
}: {
children: ReactNode;
}) => {
const [data, setData] = useState([]);
const [installations, setInstallations] = useState([]);
const [foldersAndInstallations, setFoldersAndInstallations] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [updated, setUpdated] = useState(false);
@ -67,7 +68,7 @@ const InstallationsContextProvider = ({
axiosConfig
.get('/GetAllInstallations', {})
.then((res) => {
setData(res.data);
setInstallations(res.data);
})
.catch((err: AxiosError) => {
if (err.response && err.response.status == 401) {
@ -80,14 +81,14 @@ const InstallationsContextProvider = ({
return axiosConfig
.get('/GetAllFoldersAndInstallations')
.then((res) => {
setData(res.data);
setFoldersAndInstallations(res.data);
})
.catch((err) => {
if (err.response && err.response.status == 401) {
removeToken();
}
});
}, [setData]);
}, []);
const createInstallation = useCallback(
async (formValues: Partial<I_Installation>) => {
@ -235,7 +236,8 @@ const InstallationsContextProvider = ({
return (
<InstallationsContext.Provider
value={{
data,
installations,
foldersAndInstallations,
fetchAllInstallations,
fetchAllFoldersAndInstallations,
createInstallation,

View File

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

View File

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

View File

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

View File

@ -16,7 +16,6 @@
"german": "Deutsch",
"groupTabs": "Gruppen",
"groupTree": "Gruppenbaum",
"information": "Information",
"inheritedAccess": "Vererbter Zugriff von",
"installation": "Installation",
"installationTabs": "Installationen",
@ -48,5 +47,24 @@
"live": "Live Übertragung",
"deleteInstallation": "Installation löschen",
"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",
"applyChanges": "Apply changes",
"deleteInstallation": "Delete Installation",
"country": "Country",
"customerName": "Customer name",
"english": "English",
@ -53,5 +51,24 @@
"live": "Live View",
"deleteInstallation": "Delete Installation",
"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",
"groupTabs": "Onglets de groupe",
"groupTree": "Arbre de groupe",
"information": "Informations",
"inheritedAccess": "Accès hérité de",
"installation": "Installation",
"installationTabs": "Onglets d'installation",
@ -48,5 +47,24 @@
"live": "Diffusion en direct",
"deleteInstallation": "Supprimer l'installation",
"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,
ListItemText,
Menu,
MenuItem,
Switch
MenuItem
} from '@mui/material';
import { useContext, useRef, useState } from 'react';
import { styled } from '@mui/material/styles';
@ -98,6 +97,11 @@ function HeaderMenu(props: HeaderButtonsProps) {
setOpen(false);
};
const handleLanguageSelect = (language) => {
props.onSelectLanguage(language);
handleClose();
};
return (
<>
<ListWrapper
@ -129,15 +133,14 @@ function HeaderMenu(props: HeaderButtonsProps) {
</ListItem>
</List>
</ListWrapper>
<Switch checked={darkState} onChange={handleThemeChange} />
<Menu anchorEl={ref.current} onClose={handleClose} open={isOpen}>
<MenuItem value="en" onClick={() => props.onSelectLanguage('en')}>
<MenuItem value="en" onClick={() => handleLanguageSelect('en')}>
English
</MenuItem>
<MenuItem value="de" onClick={() => props.onSelectLanguage('de')}>
<MenuItem value="de" onClick={() => handleLanguageSelect('de')}>
German
</MenuItem>
<MenuItem value="fr" onClick={() => props.onSelectLanguage('fr')}>
<MenuItem value="fr" onClick={() => handleLanguageSelect('fr')}>
French
</MenuItem>
</Menu>

View File

@ -18,7 +18,7 @@ import SidebarMenu from './SidebarMenu';
const SidebarWrapper = styled(Box)(
({ theme }) => `
width: ${theme.sidebar.width};
min-width: ${theme.sidebar.width};
min-width: "${theme.sidebar.width}";
color: ${theme.colors.alpha.trueWhite[70]};
position: relative;
z-index: 7;
@ -64,8 +64,7 @@ function Sidebar() {
alt="innovenergy logo"
style={{
maxWidth: '150px', // Maximum width for the image
maxHeight: '150px', // Maximum height for the image
marginLeft: '50px' // Adjust the value as needed
maxHeight: '150px' // Maximum height for the image
}}
/>
</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)(
({ 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,
boxShadow:
'2px 0 3px rgba(159, 162, 191, .18), 1px 0 1px rgba(159, 162, 191, 0.32)',
width: '290px'
width: '200px'
},
header: {
height: '80px',