Merge branch 'marios'

This commit is contained in:
Kim 2023-09-18 15:51:47 +02:00
commit d04b431c9f
120 changed files with 44784 additions and 5 deletions

1
csharp/App/Backend/deploy.sh Executable file
View File

@ -0,0 +1 @@
dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/backend && ssh ubuntu@194.182.190.208 'sudo systemctl restart backend'

View File

@ -0,0 +1,4 @@
# .estlintignore file
dist
build
node_modules/

View File

@ -0,0 +1,69 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:react/recommended",
"airbnb-typescript",
"plugin:react/jsx-runtime",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 11,
"project": "./tsconfig.json",
"sourceType": "module"
},
"plugins": ["react", "@typescript-eslint"],
"settings": {
"react": {
"pragma": "React",
"fragment": "Fragment",
"version": "detect"
}
},
"rules": {
"prettier/prettier": "off",
"react/jsx-filename-extension": "off",
"import/no-unresolved": "off",
"import/extensions": "off",
"react/display-name": "off",
"@typescript-eslint/comma-dangle": "off",
"import/prefer-default-export": "off",
"jsx-a11y/anchor-is-valid": "off",
"comma-dangle": "off",
"max-len": "off",
"no-console": "off",
"no-param-reassign": "off",
"no-plusplus": "off",
"no-return-assign": "off",
"object-curly-newline": "off",
"react/jsx-props-no-spreading": "off",
"react/react-in-jsx-scope": "off",
"react/require-default-props": "off",
"typescript-eslint/no-unused-vars": "off",
"import/no-extraneous-dependencies": "off",
"react/no-unescaped-entities": "off",
"react/forbid-prop-types": "off",
"react/jsx-max-props-per-line": [
1,
{
"maximum": 2,
"when": "multiline"
}
],
"indent": "off",
"@typescript-eslint/indent": [0],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": ["off"],
"@typescript-eslint/no-unused-vars": ["off"],
"@typescript-eslint/no-shadow": ["off"],
"@typescript-eslint/dot-notation": ["off"],
"react/prop-types": ["off"],
"@typescript-eslint/naming-convention": ["off"]
}
}

26
typescript/frontend-marios2/.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
/.env
/.env.local
/.env.development.local
/.env.test.local
/.env.production.local
.idea
features.html
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,9 @@
{
"bracketSpacing": true,
"printWidth": 80,
"singleQuote": true,
"trailingComma": "none",
"tabWidth": 2,
"useTabs": false,
"bracketSameLine": false
}

View File

@ -0,0 +1,21 @@
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.

View File

@ -0,0 +1 @@
npm run build && rsync -rv .* ubuntu@194.182.190.208:~/frontend/ && ssh ubuntu@194.182.190.208 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.innov.energy/html/' && ssh ubuntu@194.182.190.208 'sudo npm install -g serve'

30307
typescript/frontend-marios2/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,76 @@
{
"name": "InnovEnergy",
"version": "2.0.0",
"title": "InnovEnergy",
"private": false,
"dependencies": {
"@emotion/react": "11.9.0",
"@emotion/styled": "11.8.1",
"@mui/icons-material": "5.8.2",
"@mui/lab": "5.0.0-alpha.84",
"@mui/material": "5.8.2",
"@mui/styles": "5.8.0",
"@types/react": "17.0.40",
"@types/react-dom": "17.0.13",
"apexcharts": "3.35.3",
"axios": "^1.5.0",
"clsx": "1.1.1",
"date-fns": "2.28.0",
"history": "5.3.0",
"linq-to-typescript": "^11.0.0",
"nprogress": "0.2.0",
"numeral": "2.0.6",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-apexcharts": "1.4.0",
"react-custom-scrollbars-2": "4.4.0",
"react-dom": "17.0.2",
"react-helmet-async": "1.3.0",
"react-intl": "^6.4.4",
"react-router": "6.3.0",
"react-router-dom": "6.3.0",
"react-scripts": "5.0.1",
"rxjs": "^7.8.1",
"simplytyped": "^3.3.0",
"stylis": "4.1.1",
"stylis-plugin-rtl": "2.1.1",
"typescript": "4.7.3",
"universal-cookie": "^6.1.0",
"web-vitals": "2.1.4",
"yum": "^0.1.1",
"yup": "^1.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject",
"lint": "eslint .",
"lint:fix": "eslint --fix",
"format": "prettier --write \"./**/*.{ts,tsx,js,jsx,json}\" --config ./.prettierrc"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.27.0",
"@typescript-eslint/parser": "5.27.0",
"eslint": "8.17.0",
"eslint-config-airbnb-typescript": "17.0.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jsx-a11y": "6.5.1",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-react": "7.30.0",
"eslint-plugin-react-hooks": "4.5.0",
"prettier": "2.6.2"
}
}

View File

@ -0,0 +1 @@
/* /index.html 200

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link href="%PUBLIC_URL%/favicon.png" rel="shortcut icon"/>
<meta
content="width=device-width, initial-scale=1, shrink-to-fit=no"
name="viewport"
/>
<meta content="#1975ff" name="theme-color"/>
<link href="%PUBLIC_URL%/manifest.json" rel="manifest"/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400&display=swap"
rel="stylesheet"
/>
<title>InnovEnergy</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,30 @@
{
"theme_color": "#1975ff",
"background_color": "#f2f5f9",
"display": "standalone",
"start_url": ".",
"short_name": "InnovEnergy",
"name": "InnovEnergy",
"icons": [
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,210 @@
import { Navigate, Route, Routes } from 'react-router-dom';
import { CssBaseline } from '@mui/material';
import ThemeProvider from './theme/ThemeProvider';
import React, { lazy, Suspense, useContext, useState } from 'react';
import { UserContext } from './contexts/userContext';
import Login from './components/login';
import { IntlProvider } from 'react-intl';
import en from './lang/en.json';
import de from './lang/de.json';
import fr from './lang/fr.json';
import SuspenseLoader from './components/SuspenseLoader';
import { RouteObject } from 'react-router';
import BaseLayout from './layouts/BaseLayout';
import SidebarLayout from './layouts/SidebarLayout';
import { TokenContext } from './contexts/tokenContext';
import ResetPassword from './components/ResetPassword';
import ForgotPassword from './components/ForgotPassword';
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);
const [language, setLanguage] = useState('en');
const getTranslations = () => {
switch (language) {
case 'en':
return en;
case 'de':
return de;
case 'fr':
return fr;
}
};
const onForgotPassword = () => {
setForgotPassword(true);
};
const resetPassword = () => {
setForgotPassword(false);
};
const Loader = (Component) => (props) =>
(
<Suspense fallback={<SuspenseLoader />}>
<Component {...props} />
</Suspense>
);
// Dashboards
const Installations = Loader(
lazy(() => import('src/content/dashboards/Installations/'))
);
const ResetPassword = Loader(
lazy(() => import('src/components/ResetPassword'))
);
const Login = Loader(lazy(() => import('src/components/login')));
const Users = Loader(lazy(() => import('src/content/dashboards/Users')));
// Status
const Status404 = Loader(
lazy(() => import('src/content/pages/Status/Status404'))
);
const Status500 = Loader(
lazy(() => import('src/content/pages/Status/Status500'))
);
const StatusComingSoon = Loader(
lazy(() => import('src/content/pages/Status/ComingSoon'))
);
const StatusMaintenance = Loader(
lazy(() => import('src/content/pages/Status/Maintenance'))
);
const routes: RouteObject[] = [
{
path: '',
element: <BaseLayout />,
children: [
{
path: '/',
element: <Navigate to="installations" replace />
},
{
path: 'status',
children: [
{
path: '',
element: <Navigate to="404" replace />
},
{
path: '404',
element: <Status404 />
},
{
path: '500',
element: <Status500 />
},
{
path: 'maintenance',
element: <StatusMaintenance />
},
{
path: 'coming-soon',
element: <StatusComingSoon />
}
]
},
{
path: '*',
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) {
return (
<ThemeProvider>
<CssBaseline />
<ForgotPassword resetPassword={resetPassword} />
</ThemeProvider>
);
}
if (!token) {
return (
<ThemeProvider>
<CssBaseline />
<Login onForgotPassword={onForgotPassword}></Login>
</ThemeProvider>
);
}
if (token && currentUser?.mustResetPassword) {
return (
<ThemeProvider>
<CssBaseline />
<ResetPassword></ResetPassword>
</ThemeProvider>
);
}
return (
<ThemeProvider>
<IntlProvider
messages={getTranslations()}
locale={language}
defaultLocale="en"
>
<CssBaseline />
<Routes>
{routes.map((route, index) => (
<Route key={index} path={route.path} element={route.element}>
{route.children &&
route.children.map((childRoute, childIndex) => (
<Route
key={childIndex}
path={childRoute.path}
element={childRoute.element}
/>
))}
</Route>
))}
</Routes>
</IntlProvider>
</ThemeProvider>
);
}
export default App;

View File

@ -1,11 +1,11 @@
import axios from 'axios';
export const axiosConfigWithoutToken = axios.create({
baseURL: 'https://localhost:7087/api'
baseURL: 'https://monitor.innov.energy/api'
});
const axiosConfig = axios.create({
baseURL: 'https://localhost:7087/api'
baseURL: 'https://monitor.innov.energy/api'
});
axiosConfig.defaults.params = {};

View File

@ -0,0 +1,44 @@
import { Box, Container, Link, styled, Typography } from '@mui/material';
const FooterWrapper = styled(Container)(
({ theme }) => `
margin-top: ${theme.spacing(4)};
`
);
function Footer() {
return (
<FooterWrapper className="footer-wrapper">
<Box
pb={4}
display={{ xs: 'block', md: 'flex' }}
alignItems="center"
textAlign={{ xs: 'center', md: 'left' }}
justifyContent="space-between"
>
<Box>
<Typography variant="subtitle1">
&copy; 2023 - InnovEnergy AG
</Typography>
</Box>
<Typography
sx={{
pt: { xs: 2, md: 0 }
}}
variant="subtitle1"
>
Crafted by{' '}
<Link
href="https://www.innov.energy/"
target="_blank"
rel="noopener noreferrer"
>
InnovEnergy AG
</Link>
</Typography>
</Box>
</FooterWrapper>
);
}
export default Footer;

View File

@ -0,0 +1,230 @@
import React, { useContext, useState } from 'react';
import {
Box,
Button,
CircularProgress,
Container,
Grid,
Modal,
TextField,
Typography,
useTheme
} from '@mui/material';
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png';
import { UserContext } from 'src/contexts/userContext';
import { TokenContext } from 'src/contexts/tokenContext';
import Avatar from '@mui/material/Avatar';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import axiosConfig from 'src/Resources/axiosConfig';
interface ForgotPasswordPromps {
resetPassword: () => void;
}
function ForgotPassword(props: ForgotPasswordPromps) {
const [username, setUsername] = useState('');
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [errorModalOpen, setErrorModalOpen] = useState(false);
const theme = useTheme();
const context = useContext(UserContext);
if (!context) {
return null;
}
const { currentUser, setUser, removeUser } = context;
const tokencontext = useContext(TokenContext);
const { token, setNewToken, removeToken } = tokencontext;
const handleUsernameChange = (e) => {
const { name, value } = e.target;
setUsername(value);
};
const handleReturn = () => {
setOpen(false);
props.resetPassword();
};
const handleSubmit = () => {
setLoading(true);
axiosConfig
.post(`/ResetPasswordRequest?username=${username}`)
.then((response) => {
if (response) {
setLoading(false);
setOpen(true);
}
})
.catch((error) => {
setLoading(false);
setErrorModalOpen(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
};
return (
<>
<Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://www.innov.energy/de/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
</a>
</Grid>
</Grid>
</Container>
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: 500,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 6,
position: 'absolute',
top: '30%',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
>
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Provide your username
</Typography>
<Box
component="form"
noValidate
sx={{
mt: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<TextField
label="User Name"
variant="outlined"
type="username"
value={username}
onChange={handleUsernameChange}
fullWidth
margin="normal"
required
sx={{ width: 350 }}
/>
{loading && <CircularProgress sx={{ color: '#ffc04d' }} />}
<Button
sx={{
mt: 3,
mb: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
'&:hover': { bgcolor: '#f7b34d' }
}}
variant="contained"
fullWidth={true}
color="primary"
onClick={handleSubmit}
>
Submit
</Button>
<Modal
open={errorModalOpen}
onClose={() => setErrorModalOpen(false)}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 340,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography variant="body1" gutterBottom>
Username is wrong. Please try again.
</Typography>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={() => setErrorModalOpen(false)}
>
Close
</Button>
</Box>
</Modal>
<Modal
open={open}
onClose={() => setOpen(false)}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 340,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography variant="body1" gutterBottom>
Mail sent successfully.
</Typography>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={handleReturn}
>
Close
</Button>
</Box>
</Modal>
</Box>
</Box>
</>
);
}
export default ForgotPassword;

View File

@ -0,0 +1,95 @@
import { FC, ReactNode } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
interface LabelProps {
className?: string;
color?:
| 'primary'
| 'black'
| 'secondary'
| 'error'
| 'warning'
| 'success'
| 'info';
children?: ReactNode;
}
const LabelWrapper = styled('span')(
({ theme }) => `
background-color: ${theme.colors.alpha.black[5]};
padding: ${theme.spacing(0.5, 1)};
font-size: ${theme.typography.pxToRem(13)};
border-radius: ${theme.general.borderRadius};
display: inline-flex;
align-items: center;
justify-content: center;
max-height: ${theme.spacing(3)};
&.MuiLabel {
&-primary {
background-color: ${theme.colors.primary.lighter};
color: ${theme.palette.primary.main}
}
&-black {
background-color: ${theme.colors.alpha.black[100]};
color: ${theme.colors.alpha.white[100]};
}
&-secondary {
background-color: ${theme.colors.secondary.lighter};
color: ${theme.palette.secondary.main}
}
&-success {
background-color: ${theme.colors.success.lighter};
color: ${theme.palette.success.main}
}
&-warning {
background-color: ${theme.colors.warning.lighter};
color: ${theme.palette.warning.main}
}
&-error {
background-color: ${theme.colors.error.lighter};
color: ${theme.palette.error.main}
}
&-info {
background-color: ${theme.colors.info.lighter};
color: ${theme.palette.info.main}
}
}
`
);
const Label: FC<LabelProps> = ({
className,
color = 'secondary',
children,
...rest
}) => {
return (
<LabelWrapper className={'MuiLabel-' + color} {...rest}>
{children}
</LabelWrapper>
);
};
Label.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
color: PropTypes.oneOf([
'primary',
'black',
'secondary',
'error',
'warning',
'success',
'info'
])
};
export default Label;

View File

@ -0,0 +1,126 @@
import {
Badge,
Box,
styled,
Tooltip,
tooltipClasses,
TooltipProps,
useTheme
} from '@mui/material';
import { Link } from 'react-router-dom';
const LogoWrapper = styled(Link)(
({ theme }) => `
color: ${theme.palette.text.primary};
display: flex;
text-decoration: none;
width: 53px;
margin: 0 auto;
font-weight: ${theme.typography.fontWeightBold};
`
);
const LogoSignWrapper = styled(Box)(
() => `
width: 52px;
height: 38px;
`
);
const LogoSign = styled(Box)(
({ theme }) => `
background: ${theme.general.reactFrameworkColor};
width: 18px;
height: 18px;
border-radius: ${theme.general.borderRadiusSm};
position: relative;
transform: rotate(45deg);
top: 3px;
left: 17px;
&:after,
&:before {
content: "";
display: block;
width: 18px;
height: 18px;
position: absolute;
top: -1px;
right: -20px;
transform: rotate(0deg);
border-radius: ${theme.general.borderRadiusSm};
}
&:before {
background: ${theme.palette.primary.main};
right: auto;
left: 0;
top: 20px;
}
&:after {
background: ${theme.palette.secondary.main};
}
`
);
const LogoSignInner = styled(Box)(
({ theme }) => `
width: 16px;
height: 16px;
position: absolute;
top: 12px;
left: 12px;
z-index: 5;
border-radius: ${theme.general.borderRadiusSm};
background: ${theme.header.background};
`
);
const TooltipWrapper = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.colors.alpha.trueWhite[100],
color: theme.palette.getContrastText(theme.colors.alpha.trueWhite[100]),
fontSize: theme.typography.pxToRem(12),
fontWeight: 'bold',
borderRadius: theme.general.borderRadiusSm,
boxShadow:
'0 .2rem .8rem rgba(7,9,25,.18), 0 .08rem .15rem rgba(7,9,25,.15)'
},
[`& .${tooltipClasses.arrow}`]: {
color: theme.colors.alpha.trueWhite[100]
}
}));
function Logo() {
const theme = useTheme();
return (
<TooltipWrapper title="InnovEnergy" arrow>
<LogoWrapper to="/overview">
<Badge
sx={{
'.MuiBadge-badge': {
fontSize: theme.typography.pxToRem(11),
right: -2,
top: 8
}
}}
overlap="circular"
color="success"
badgeContent="2.0"
>
<LogoSignWrapper>
<LogoSign>
<LogoSignInner />
</LogoSign>
</LogoSignWrapper>
</Badge>
</LogoWrapper>
</TooltipWrapper>
);
}
export default Logo;

View File

@ -0,0 +1,53 @@
import { FC } from 'react';
import PropTypes from 'prop-types';
import AddTwoToneIcon from '@mui/icons-material/AddTwoTone';
import { Typography, Button, Grid } from '@mui/material';
interface PageTitleProps {
heading?: string;
subHeading?: string;
docs?: string;
}
const PageTitle: FC<PageTitleProps> = ({
heading = '',
subHeading = '',
docs = '',
...rest
}) => {
return (
<Grid
container
justifyContent="space-between"
alignItems="center"
{...rest}
>
<Grid item>
<Typography variant="h3" component="h3" gutterBottom>
{heading}
</Typography>
<Typography variant="subtitle2">{subHeading}</Typography>
</Grid>
<Grid item>
<Button
href={docs}
target="_blank"
rel="noopener noreferrer"
sx={{ mt: { xs: 2, md: 0 } }}
variant="contained"
startIcon={<AddTwoToneIcon fontSize="small" />}
>
{heading} Documentation
</Button>
</Grid>
</Grid>
);
};
PageTitle.propTypes = {
heading: PropTypes.string,
subHeading: PropTypes.string,
docs: PropTypes.string
};
export default PageTitle;

View File

@ -0,0 +1,27 @@
import { FC, ReactNode } from 'react';
import PropTypes from 'prop-types';
import { Box, Container, styled } from '@mui/material';
const PageTitle = styled(Box)(
({ theme }) => `
padding: ${theme.spacing(4)};
`
);
interface PageTitleWrapperProps {
children?: ReactNode;
}
const PageTitleWrapper: FC<PageTitleWrapperProps> = ({ children }) => {
return (
<PageTitle className="MuiPageTitle-wrapper">
<Container maxWidth="lg">{children}</Container>
</PageTitle>
);
};
PageTitleWrapper.propTypes = {
children: PropTypes.node.isRequired
};
export default PageTitleWrapper;

View File

@ -0,0 +1,214 @@
import React, { useContext, useState } from 'react';
import {
Box,
Button,
CircularProgress,
Container,
Grid,
Modal,
TextField,
Typography,
useTheme
} from '@mui/material';
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png';
import axiosConfig from 'src/Resources/axiosConfig';
import { UserContext } from 'src/contexts/userContext';
import { TokenContext } from 'src/contexts/tokenContext';
import { useNavigate } from 'react-router-dom';
import Avatar from '@mui/material/Avatar';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
function ResetPassword() {
const [username, setUsername] = useState('');
const [loading, setLoading] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [open, setOpen] = useState(false);
const theme = useTheme();
const context = useContext(UserContext);
const navigate = useNavigate();
const [password, setPassword] = useState<string>('');
const [verifypassword, setVerifyPassword] = useState<string>('');
if (!context) {
return null;
}
const { currentUser, setUser, removeUser } = context;
const tokencontext = useContext(TokenContext);
const { token, setNewToken, removeToken } = tokencontext;
const handlePasswordChange = (e) => {
const { name, value } = e.target;
setPassword(value);
};
const handleVerifiedPasswordChange = (e) => {
const { name, value } = e.target;
setVerifyPassword(value);
};
const handleSubmit = () => {
setLoading(true);
axiosConfig
.put('/UpdatePassword', undefined, {
params: { newPassword: password }
})
.then((res) => {
setLoading(false);
currentUser.mustResetPassword = false;
setUser(currentUser);
window.location.reload();
})
.catch((error) => {
setLoading(false);
setOpen(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
};
return (
<>
<Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://www.innov.energy/de/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
</a>
</Grid>
</Grid>
</Container>
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: 500,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 6,
position: 'absolute',
top: '30%',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
>
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Reset Password
</Typography>
<Box
component="form"
noValidate
sx={{
mt: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<TextField
label="Password"
variant="outlined"
type="password"
value={password}
onChange={handlePasswordChange}
fullWidth
margin="normal"
required
sx={{ width: 350 }}
/>
<TextField
label="Verify Password"
type="password"
variant="outlined"
value={verifypassword}
onChange={handleVerifiedPasswordChange}
fullWidth
margin="normal"
required
sx={{ width: 350 }}
/>
{loading && (
<CircularProgress sx={{ color: '#ffc04d', marginLeft: '170px' }} />
)}
{password != verifypassword && (
<Typography
component="h1"
variant="h5"
sx={{ color: '#FF0000', marginTop: 1 }}
>
Passwords do not match
</Typography>
)}
<Button
sx={{
mt: 3,
mb: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
'&:hover': { bgcolor: '#f7b34d' }
}}
variant="contained"
fullWidth={true}
color="primary"
onClick={handleSubmit}
>
Submit
</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 340,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography variant="body1" gutterBottom>
Reset Password failed. Please try again.
</Typography>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={() => setOpen(false)}
>
Close
</Button>
</Box>
</Modal>
</Box>
</Box>
</>
);
}
export default ResetPassword;

View File

@ -0,0 +1,46 @@
import { FC, ReactNode } from 'react';
import PropTypes from 'prop-types';
import { Scrollbars } from 'react-custom-scrollbars-2';
import { Box, useTheme } from '@mui/material';
interface ScrollbarProps {
className?: string;
children?: ReactNode;
}
const Scrollbar: FC<ScrollbarProps> = ({ className, children, ...rest }) => {
const theme = useTheme();
return (
<Scrollbars
autoHide
renderThumbVertical={() => {
return (
<Box
sx={{
width: 5,
background: `${theme.colors.alpha.black[10]}`,
borderRadius: `${theme.general.borderRadiusLg}`,
transition: `${theme.transitions.create(['background'])}`,
'&:hover': {
background: `${theme.colors.alpha.black[30]}`
}
}}
/>
);
}}
{...rest}
>
{children}
</Scrollbars>
);
};
Scrollbar.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};
export default Scrollbar;

View File

@ -0,0 +1,32 @@
import { useEffect } from 'react';
import NProgress from 'nprogress';
import { Box, CircularProgress } from '@mui/material';
function SuspenseLoader() {
useEffect(() => {
NProgress.start();
return () => {
NProgress.done();
};
}, []);
return (
<Box
sx={{
position: 'fixed',
left: 0,
top: 0,
width: '100%',
height: '100%'
}}
display="flex"
alignItems="center"
justifyContent="center"
>
<CircularProgress size={64} disableShrink thickness={3} />
</Box>
);
}
export default SuspenseLoader;

View File

@ -0,0 +1,258 @@
import React, { useContext, useState } from 'react';
import {
Box,
Button,
Checkbox,
CircularProgress,
Container,
FormControlLabel,
Grid,
Modal,
TextField,
Typography,
useTheme
} from '@mui/material';
import Avatar from '@mui/material/Avatar';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Link from '@mui/material/Link';
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png';
import { axiosConfigWithoutToken } from 'src/Resources/axiosConfig';
import Cookies from 'universal-cookie';
import { UserContext } from 'src/contexts/userContext';
import { TokenContext } from 'src/contexts/tokenContext';
import { useNavigate } from 'react-router-dom';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
interface loginPromps {
onForgotPassword: () => void;
}
function Login(props: loginPromps) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [open, setOpen] = useState(false);
const [error, setError] = useState(false);
const theme = useTheme();
const context = useContext(UserContext);
const navigate = useNavigate();
if (!context) {
return null;
}
const { currentUser, setUser, removeUser } = context;
const tokencontext = useContext(TokenContext);
const { token, setNewToken, removeToken } = tokencontext;
const cookies = new Cookies();
const handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setUsername(event.target.value);
};
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setPassword(event.target.value);
};
const handleRememberMeChange = () => {
setRememberMe(!rememberMe);
};
const handleSubmit = () => {
setLoading(true);
axiosConfigWithoutToken
.post('/Login', null, { params: { username, password } })
.then((response) => {
if (response.data && response.data.token) {
setLoading(false);
setNewToken(response.data.token);
setUser(response.data.user);
if (rememberMe) {
cookies.set('rememberedUsername', username, { path: '/' });
cookies.set('rememberedPassword', password, { path: '/' });
}
navigate('/');
}
})
.catch((error) => {
setLoading(false);
setOpen(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
};
return (
<>
<Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://www.innov.energy/de/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
</a>
</Grid>
</Grid>
</Container>
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: 500,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 6,
position: 'absolute',
top: '30%',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
>
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<Box
component="form"
noValidate
sx={{
mt: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<TextField
label="Username"
value={username}
onChange={handleUsernameChange}
fullWidth
margin="normal"
required
sx={{ width: 350 }}
/>
<TextField
label="Password"
variant="outlined"
type="password"
value={password}
onChange={handlePasswordChange}
fullWidth
margin="normal"
required
sx={{ width: 350 }}
/>
<FormControlLabel
control={
<Checkbox
checked={rememberMe}
onChange={handleRememberMeChange}
icon={<CheckBoxOutlineBlankIcon style={{ color: 'grey' }} />}
checkedIcon={<CheckBoxIcon style={{ color: '#ffc04d' }} />}
style={{ marginLeft: -175 }}
/>
}
label="Remember me"
/>
<Button
sx={{
mb: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
'&:hover': { bgcolor: '#f7b34d' }
}}
variant="contained"
fullWidth={true}
color="primary"
onClick={handleSubmit}
>
Login
</Button>
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
/>
)}
<Modal
open={open}
onClose={() => setOpen(false)}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 300,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography variant="body1" gutterBottom>
Login failed. Please try again.
</Typography>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={() => setOpen(false)}
>
Close
</Button>
</Box>
</Modal>
<Grid container>
<Grid item xs>
<Link
href=""
variant="body2"
sx={{ color: '#111111' }}
onClick={(e) => {
e.preventDefault();
props.onForgotPassword();
}}
>
Forgot password?
</Link>
</Grid>
</Grid>
</Box>
</Box>
</>
);
}
export default Login;

View File

@ -0,0 +1,200 @@
import React, { useContext, useState } from 'react';
import {
Card,
CircularProgress,
Divider,
Grid,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
useTheme
} from '@mui/material';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import Installation from './Installation';
import CancelIcon from '@mui/icons-material/Cancel';
import { LogContext } from 'src/contexts/LogContextProvider';
import { FormattedMessage } from 'react-intl';
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 handleSelectOneInstallation = (installationID: number): void => {
if (selectedInstallation != installationID) {
setSelectedInstallation(installationID);
} else {
setSelectedInstallation(-1);
}
};
const theme = useTheme();
const findInstallation = (id: number) => {
return props.installations.find((installation) => installation.id === id);
};
const handleRowMouseEnter = (id: number) => {
setHoveredRow(id);
};
const handleRowMouseLeave = () => {
setHoveredRow(-1);
};
return (
<Grid container spacing={1} sx={{ marginTop: 0.1 }}>
<Grid item xs={12} md={3}>
<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="status" defaultMessage="Status" />
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.installations.map((installation) => {
const isInstallationSelected =
installation.id === selectedInstallation ? true : false;
const status = getStatus(installation.id);
const rowStyles =
isRowHovered === installation.id
? {
cursor: 'pointer',
backgroundColor: theme.colors.primary.lighter
}
: {};
return (
<TableRow
hover
key={installation.id}
selected={isInstallationSelected}
style={rowStyles}
onClick={() =>
handleSelectOneInstallation(installation.id)
}
onMouseEnter={() => handleRowMouseEnter(installation.id)}
onMouseLeave={() => handleRowMouseLeave()}
>
<TableCell padding="checkbox"></TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px' }}
>
{installation.name}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
sx={{ marginTop: '10px' }}
>
{installation.location}
</Typography>
</TableCell>
<TableCell>
<div
style={{
display: 'flex',
alignItems: 'center',
marginLeft: '15px'
}}
>
{status === -1 ? (
<CancelIcon
style={{
width: '23px',
height: '23px',
color: 'red',
borderRadius: '50%'
}}
/>
) : (
''
)}
{status === -2 ? (
<CircularProgress
size={20}
sx={{
color: '#f7b34d'
}}
/>
) : (
''
)}
<div
style={{
width: '20px',
height: '20px',
borderRadius: '50%',
backgroundColor:
status === 2
? 'red'
: status === 1
? 'orange'
: status === -1 || status === -2
? 'transparent'
: 'green'
}}
/>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</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>
);
};
export default FlatInstallationView;

View File

@ -0,0 +1,480 @@
import React, { ChangeEvent, useContext, useEffect, useState } from 'react';
import {
Alert,
Box,
Card,
CardContent,
CircularProgress,
Container,
Grid,
IconButton,
Tab,
Tabs,
TextField,
useTheme
} from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import Button from '@mui/material/Button';
import { TokenContext } from 'src/contexts/tokenContext';
import { UserContext } from 'src/contexts/userContext';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import AccessContextProvider from 'src/contexts/AccessContextProvider';
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 { LogContext } from 'src/contexts/LogContextProvider';
import LiveView from '../LiveView/LiveView';
import { FormattedMessage } from 'react-intl';
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: 'manage',
label: (
<FormattedMessage id="manageAccess" defaultMessage="Manage Access" />
)
},
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live View" />
},
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
}
];
const theme = useTheme();
const [currentTab, setCurrentTab] = useState<string>(tabs[0].value);
const [formValues, setFormValues] = useState(props.current_installation);
const requiredFields = ['name', 'region', 'location', 'country'];
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const installationContext = useContext(InstallationsContext);
const {
updateInstallation,
loading,
setLoading,
error,
setError,
updated,
setUpdated,
deleteInstallation
} = installationContext;
const [warnings, setWarnings] = useState<Notification[]>([]);
const [errors, setErrors] = useState<Notification[]>([]);
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);
});
}
};
if (formValues == undefined) {
return null;
}
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value);
setError(false);
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSubmit = (e) => {
setLoading(true);
setError(false);
updateInstallation(formValues, props.type);
};
const handleDelete = (e) => {
setLoading(true);
setError(false);
deleteInstallation(formValues, props.type);
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!formValues[field]) {
return false;
}
}
return true;
};
useEffect(() => {
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 now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
fetchData(now, s3Credentials)
.then((res) => {
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()
});
}
}
}
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);
}, []);
return (
<>
<Grid item xs={12} md={9} style={props.style}>
<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 === 'installation' && (
<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={
<FormattedMessage
id="customerName"
defaultMessage="Customer Name"
/>
}
name="name"
value={formValues.name}
onChange={handleChange}
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="region"
defaultMessage="Region"
/>
}
name="region"
value={formValues.region}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="location"
defaultMessage="Location"
/>
}
name="location"
value={formValues.location}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="country"
defaultMessage="Country"
/>
}
name="country"
value={formValues.country}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="orderNumbers"
defaultMessage="Order Numbers"
/>
}
name="orderNumbers"
value={formValues.orderNumbers}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleSubmit}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
<FormattedMessage
id="applyChanges"
defaultMessage="Apply Changes"
/>
</Button>
)}
{currentUser.hasWriteAccess && (
<Button
variant="contained"
onClick={handleDelete}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="deleteInstallation"
defaultMessage="Delete Installation"
/>
</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>
</Container>
)}
{currentTab === 'manage' && currentUser.hasWriteAccess && (
<AccessContextProvider>
<Access
currentResource={formValues}
resourceType={props.type}
></Access>
</AccessContextProvider>
)}
{currentTab === 'live' && (
<LiveView
warnings={warnings}
errors={errors}
errorLoadingS3Data={errorLoadingS3Data}
></LiveView>
)}
{currentTab === 'log' && (
<Log
warnings={warnings}
errors={errors}
errorLoadingS3Data={errorLoadingS3Data}
></Log>
)}
</Grid>
</Card>
</Grid>
</>
);
}
export default Installation;

View File

@ -0,0 +1,59 @@
import { useContext, useEffect, useState } from 'react';
import {
FormControl,
Grid,
InputAdornment,
TextField,
useTheme
} from '@mui/material';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
import FlatInstallationView from 'src/content/dashboards/Installations/FlatInstallationView';
function InstallationSearch() {
const theme = useTheme();
const [searchTerm, setSearchTerm] = useState('');
const { data, fetchAllInstallations } = useContext(InstallationsContext);
useEffect(() => {
fetchAllInstallations();
}, []);
const [filteredData, setFilteredData] = useState(data);
useEffect(() => {
const filtered = data.filter(
(item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.location.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredData(filtered);
}, [searchTerm, data]);
return (
<>
<Grid container spacing={4}>
<Grid item xs={12} md={3}>
<FormControl variant="outlined" fullWidth>
<TextField
placeholder="Search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchTwoToneIcon />
</InputAdornment>
)
}}
/>
</FormControl>
</Grid>
</Grid>
<FlatInstallationView installations={filteredData} />
</>
);
}
export default InstallationSearch;

View File

@ -0,0 +1,88 @@
import { ChangeEvent, 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 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';
function InstallationTabs() {
const theme = useTheme();
const [currentTab, setCurrentTab] = useState<string>('flat');
const tabs = [
{
value: 'flat',
label: 'Flat view',
icon: <ListIcon id="mode-toggle-button-list-icon" />,
component: {}
},
{
value: 'tree',
label: 'Tree view',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
}
];
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value);
};
return (
<UsersContextProvider>
<Container maxWidth="xl" sx={{ marginTop: '20px' }}>
<TabsContainerWrapper>
<Tabs
onChange={handleTabsChange}
value={currentTab}
variant="scrollable"
scrollButtons="auto"
textColor="primary"
indicatorColor="primary"
>
{tabs.map((tab) => (
<Tab key={tab.value} value={tab.value} icon={tab.icon} />
))}
</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>
</Container>
<Footer />
</UsersContextProvider>
);
}
export default InstallationTabs;

View File

@ -0,0 +1,217 @@
import React, { useContext, useState } from 'react';
import {
Alert,
Box,
CircularProgress,
IconButton,
Modal,
TextField,
useTheme
} from '@mui/material';
import Button from '@mui/material/Button';
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';
interface installationFormProps {
cancel: () => void;
submit: () => void;
parentid: number;
}
function installationForm(props: installationFormProps) {
const theme = useTheme();
const [open, setOpen] = useState(true);
const [formValues, setFormValues] = useState<Partial<I_Installation>>({
name: '',
region: '',
location: '',
country: '',
orderNumbers: ''
});
const requiredFields = ['name', 'region', 'location', 'country'];
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const installationContext = useContext(InstallationsContext);
const { createInstallation, loading, setLoading, error, setError } =
installationContext;
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSubmit = async (e) => {
setLoading(true);
formValues.parentId = props.parentid;
const responseData = await createInstallation(formValues);
props.submit();
};
const handleCancelSubmit = (e) => {
props.cancel();
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!formValues[field]) {
return false;
}
}
return true;
};
return (
<>
<Modal
open={open}
onClose={() => {}}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '30%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 600,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4
}}
>
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '50ch' }
}}
noValidate
autoComplete="off"
>
<div>
<TextField
label="Customer Name"
name="name"
value={formValues.name}
onChange={handleChange}
fullWidth
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label="Region"
name="region"
value={formValues.region}
onChange={handleChange}
fullWidth
required
error={formValues.region === ''}
/>
</div>
<div>
<TextField
label="Location"
name="location"
value={formValues.location}
onChange={handleChange}
fullWidth
required
error={formValues.location === ''}
/>
</div>
<div>
<TextField
label="Country"
name="country"
value={formValues.country}
onChange={handleChange}
fullWidth
required
error={formValues.country === ''}
/>
</div>
<div>
<TextField
label="Order Numbers"
name="orderNumbers"
value={formValues.orderNumbers}
onChange={handleChange}
fullWidth
/>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
variant="contained"
onClick={handleSubmit}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
Submit
</Button>
<Button
variant="contained"
onClick={handleCancelSubmit}
sx={{
marginLeft: '10px'
}}
>
Cancel
</Button>
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
/>
)}
{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' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
</div>
</Box>
</Box>
</Modal>
</>
);
}
export default installationForm;

View File

@ -0,0 +1,184 @@
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

@ -0,0 +1,184 @@
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 LogProps {
warnings: Notification[];
errors: Notification[];
errorLoadingS3Data: boolean;
}
function Log(props: LogProps) {
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 Log;

View File

@ -0,0 +1,24 @@
import { DataRecord } from 'src/dataCache/data';
export interface I_CsvEntry {
value: string | number;
unit: string;
}
export const parseCsv = (text: string): DataRecord => {
const y = text
.split(/\r?\n/)
.filter((split) => split.length > 0)
.map((l) => {
return l.split(';');
});
return y
.map((fields) => {
if (isNaN(Number(fields[1])) || fields[1] === '') {
return { [fields[0]]: { value: fields[1], unit: fields[2] } };
}
return { [fields[0]]: { value: parseFloat(fields[1]), unit: fields[2] } };
})
.reduce((acc, current) => ({ ...acc, ...current }), {} as DataRecord);
};

View File

@ -0,0 +1,460 @@
import React, { Fragment, useContext, useEffect, useState } from 'react';
import {
Alert,
Box,
Container,
Divider,
FormControl,
Grid,
IconButton,
InputLabel,
ListItem,
MenuItem,
Modal,
Select,
useTheme
} from '@mui/material';
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 ListItemAvatar from '@mui/material/ListItemAvatar';
import Avatar from '@mui/material/Avatar';
import ListItemText from '@mui/material/ListItemText';
import PersonRemoveIcon from '@mui/icons-material/PersonRemove';
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';
interface AccessProps {
currentResource: I_Folder | I_Installation;
resourceType: string;
}
function Access(props: AccessProps) {
const theme = useTheme();
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const [isRowHovered, setHoveredRow] = useState(-1);
const [directButtonPressed, setDirectButtonPressed] = useState(false);
const [inheritedButtonPressed, setInheritedButtonPressed] = useState(false);
const { availableUsers, fetchAvailableUsers } = useContext(UsersContext);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
const [openFolder, setOpenFolder] = useState(false);
const [openModal, setOpenModal] = useState(false);
const grantedAccessUsers = [];
const NotGrantedAccessUsers = [];
let tempresourceType = '';
const [resourceType, setResourceType] = useState('');
const [resourceId, setResourceId] = useState('');
const accessContext = useContext(AccessContext);
const {
usersWithDirectAccess,
fetchUsersWithDirectAccessForResource,
usersWithInheritedAccess,
fetchUsersWithInheritedAccessForResource,
error,
setError,
updated,
setUpdated,
updatedmessage,
errormessage,
setErrorMessage,
setUpdatedMessage,
RevokeAccessFromResource
} = accessContext;
useEffect(() => {
if (props.resourceType == 'folder') {
tempresourceType = 'ToFolder';
setResourceType('ToFolder');
setResourceId('FolderId');
} else {
setResourceType('ToInstallation');
tempresourceType = 'ToInstallation';
setResourceId('InstallationId');
}
fetchAvailableUsers();
fetchUsersWithDirectAccessForResource(
tempresourceType,
props.currentResource.id
);
fetchUsersWithInheritedAccessForResource(
tempresourceType,
props.currentResource.id
);
setDirectButtonPressed(false);
}, [props.currentResource, props.resourceType]);
const revokeAccessToUser = (id: number, name: string) => {
RevokeAccessFromResource(
resourceType,
id,
resourceId,
props.currentResource.id,
name
);
};
const handleGrantAccess = () => {
setOpenModal(true);
};
const handleDirectButtonPressed = () => {
setDirectButtonPressed(!directButtonPressed);
};
const handleInheritedButtonPressed = () => {
setInheritedButtonPressed(!inheritedButtonPressed);
};
const handleFolderChange = (event) => {
setSelectedUsers(event.target.value);
};
const handleOpenFolder = () => {
setOpenFolder(true);
};
const handleCloseFolder = () => {
setOpenFolder(false);
};
const handleCancel = () => {
setOpenModal(false);
};
const handleSubmit = async () => {
for (const userName of selectedUsers) {
const userId = availableUsers.find((user) => user.name === userName).id;
await axiosConfig
.post(
`/GrantUserAccess${resourceType}?UserId=${userId}&${resourceId}=${props.currentResource.id}`
)
.then((response) => {
if (response) {
grantedAccessUsers.push(userName);
}
})
.catch((error) => {
NotGrantedAccessUsers.push(userName);
});
}
fetchUsersWithDirectAccessForResource(
resourceType,
props.currentResource.id
);
fetchUsersWithInheritedAccessForResource(
resourceType,
props.currentResource.id
);
setOpenModal(false);
if (NotGrantedAccessUsers.length > 0) {
setError(true);
setErrorMessage(
'Unable to grant access to: ' + NotGrantedAccessUsers.join(', ')
);
}
if (grantedAccessUsers.length > 0) {
setUpdatedMessage(
'Granted access to users: ' + grantedAccessUsers.join(', ')
);
setUpdated(true);
setTimeout(() => {
setUpdated(false);
}, 3000);
}
};
return (
<Container maxWidth="xl">
<Grid container>
<Grid item xs={12} md={12}>
{updated && (
<Alert
severity="success"
sx={{
mt: 1
}}
>
{updatedmessage}
<IconButton
color="inherit"
size="small"
onClick={() => setUpdated(false)}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
{error && (
<Alert
severity="error"
sx={{
mt: 1
}}
>
{errormessage}
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
<Modal
open={openModal}
onClose={() => {}}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '30%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 500,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4
}}
>
<Box
component="form"
sx={{
textAlign: 'center'
}}
noValidate
autoComplete="off"
>
<FormControl fullWidth>
<InputLabel
sx={{
fontSize: 14
//backgroundColor: 'white'
}}
>
Select users
</InputLabel>
<Select
multiple
value={selectedUsers}
onChange={handleFolderChange}
open={openFolder}
onClose={handleCloseFolder}
onOpen={handleOpenFolder}
renderValue={(selected) => (
<div>
{selected.map((folder) => (
<span key={folder}>{folder}, </span>
))}
</div>
)}
>
{availableUsers.map((user) => (
<MenuItem key={user.id} value={user.name}>
{user.name}
</MenuItem>
))}
<Button
sx={{
marginLeft: '170px',
marginTop: '10px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
onClick={handleCloseFolder}
>
Ok
</Button>
</Select>
</FormControl>
<Button
sx={{
marginTop: '20px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
onClick={handleSubmit}
>
Submit
</Button>
<Button
variant="contained"
onClick={handleCancel}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
>
Cancel
</Button>
</Box>
</Box>
</Modal>
<Button
variant="contained"
onClick={handleGrantAccess}
sx={{
marginTop: '20px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
Grant Access
</Button>
</Grid>
<Grid item xs={12} md={12}>
<Button
variant="contained"
onClick={handleDirectButtonPressed}
sx={{ marginTop: '20px' }}
>
Users with Direct Access
</Button>
{directButtonPressed &&
usersWithDirectAccess.map((user) => {
return (
<Fragment key={user.id}>
<ListItem
secondaryAction={
currentUser.hasWriteAccess && (
<IconButton
onClick={() => revokeAccessToUser(user.id, user.name)}
edge="end"
>
<PersonRemoveIcon />
</IconButton>
)
}
>
<ListItemAvatar>
<Avatar>
<PersonIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={user.name} />
</ListItem>
<Divider />
</Fragment>
);
})}
{directButtonPressed && usersWithDirectAccess.length == 0 && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center',
marginTop: '20px'
}}
>
There are no users with direct access to this {props.resourceType}
.
<IconButton
color="inherit"
size="small"
sx={{ marginLeft: '4px' }}
></IconButton>
</Alert>
)}
</Grid>
<Grid item xs={12} md={12}>
<Button
variant="contained"
onClick={handleInheritedButtonPressed}
sx={{ marginTop: '20px', marginBottom: '20px' }}
>
Users with Inherited Access
</Button>
{inheritedButtonPressed && usersWithInheritedAccess.length == 0 && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center',
marginBottom: '20px'
}}
>
There are no users with inherited access to this{' '}
{props.resourceType}.
<IconButton
color="inherit"
size="small"
sx={{ marginLeft: '4px' }}
></IconButton>
</Alert>
)}
{inheritedButtonPressed &&
usersWithInheritedAccess.map((inheritedUser) => {
return (
<Fragment key={inheritedUser.user.id}>
<ListItem
secondaryAction={
currentUser.hasWriteAccess && (
<IconButton
onClick={() =>
revokeAccessToUser(
inheritedUser.user.id,
inheritedUser.user.name
)
}
edge="end"
>
<PersonRemoveIcon />
</IconButton>
)
}
>
<ListItemAvatar>
<Avatar>
<PersonIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={inheritedUser.user.name} />
</ListItem>
<Divider />
</Fragment>
);
})}
</Grid>
</Grid>
</Container>
);
}
export default Access;

View File

@ -0,0 +1,135 @@
import React, { ReactNode } from 'react';
import { CircularProgress, ListItemIcon, useTheme } from '@mui/material';
import { TreeItem } from '@mui/lab';
import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
import FolderIcon from '@mui/icons-material/Folder';
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';
interface CustomTreeItemProps {
node: I_Installation | I_Folder;
parent_id: number;
children?: ReactNode;
handleSelectedInstallation: () => void;
status: number;
}
const useTreeItemStyles = makeStyles((theme) => ({
label: {
fontWeight: 'inherit',
color: 'inherit'
},
labelRoot: {
display: 'flex',
alignItems: 'center'
},
labelIcon: {
marginRight: -20
},
labelText: {
fontWeight: 'inherit',
flexGrow: 1
}
}));
function CustomTreeItem(props: CustomTreeItemProps) {
const theme = useTheme();
const classes = useTreeItemStyles();
const renderIcon = () => {
if (props.node.type === 'Folder') {
return <FolderIcon />;
} else if (props.node.type === 'Installation') {
return <InsertDriveFileIcon />;
}
return null;
};
return (
<TreeItem
nodeId={
props.node.id.toString() + props.parent_id.toString() + props.node.type
}
label={
<div className={classes.labelRoot}>
<ListItemIcon color="inherit" className={classes.labelIcon}>
{renderIcon()}
</ListItemIcon>
<Typography
fontWeight="bold"
variant="body2"
className={classes.labelText}
sx={{ marginTop: '5px' }}
>
{props.node.name}
</Typography>
{props.node.type === 'Installation' && (
<div>
{props.status === -1 ? (
<CancelIcon
style={{
width: '23px',
height: '23px',
color: 'red',
borderRadius: '50%',
marginRight: '40px',
marginTop: '30px'
}}
/>
) : (
''
)}
{props.status === -2 ? (
<CircularProgress
size={20}
sx={{
color: '#f7b34d',
marginRight: '40px',
marginTop: '30px'
}}
/>
) : (
''
)}
<div
style={{
width: '20px',
height: '20px',
borderRadius: '50%',
marginRight: '40px',
backgroundColor:
props.status === 2
? 'red'
: props.status === 1
? 'orange'
: props.status === -1 || props.status === -2
? 'transparent'
: 'green'
}}
/>
</div>
)}
</div>
}
sx={{
'.MuiTreeItem-content': {
width: 'inherit',
borderRadius: '4px',
height: '50px',
'&:hover': {
cursor: 'pointer',
backgroundColor: theme.colors.primary.lighter
}
}
}}
onClick={() => props.handleSelectedInstallation()}
>
{props.children}
</TreeItem>
);
}
export default CustomTreeItem;

View File

@ -0,0 +1,349 @@
import React, { ChangeEvent, useContext, useEffect, useState } from 'react';
import {
Alert,
Box,
Card,
CardContent,
CircularProgress,
Container,
Grid,
IconButton,
Tab,
Tabs,
TextField,
useTheme
} from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material';
import { I_Folder } from 'src/interfaces/InstallationTypes';
import Button from '@mui/material/Button';
import FolderForm from './folderForm';
import InstallationForm from '../Installations/installationForm';
import { TokenContext } from 'src/contexts/tokenContext';
import { UserContext } from 'src/contexts/userContext';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import AccessContextProvider from 'src/contexts/AccessContextProvider';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import Access from '../ManageAccess/Access';
interface singleFolderProps {
current_folder: I_Folder;
}
function Folder(props: singleFolderProps) {
const theme = useTheme();
const [currentTab, setCurrentTab] = useState<string>('folder');
const [formValues, setFormValues] = useState(props.current_folder);
const [openModalFolder, setOpenModalFolder] = useState(false);
const [openModalInstallation, setOpenModalInstallation] = useState(false);
const requiredFields = ['name'];
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const [isRowHovered, setHoveredRow] = useState(-1);
const [selectedUser, setSelectedUser] = useState<number>(-1);
const selectedBulkActions = selectedUser !== -1;
const installationContext = useContext(InstallationsContext);
const {
loading,
setLoading,
error,
setError,
updated,
setUpdated,
updateFolder,
deleteFolder
} = installationContext;
useEffect(() => {
setFormValues(props.current_folder);
}, [props.current_folder]);
if (formValues == undefined) {
return null;
}
const tabs = [
{ value: 'folder', label: 'Folder' },
{ value: 'manage', label: 'Manage Access' }
];
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value);
setError(false);
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSelectOneUser = (installationID: number): void => {
if (selectedUser != installationID) {
setSelectedUser(installationID);
} else {
setSelectedUser(-1);
}
};
const handleRowMouseEnter = (id: number) => {
setHoveredRow(id);
};
const handleRowMouseLeave = () => {
setHoveredRow(-1);
};
const handleFolderInformationUpdate = (e) => {
setLoading(true);
setError(false);
updateFolder(formValues);
};
const handleNewInstallationInsertion = (e) => {
setOpenModalInstallation(true);
};
const handleNewFolderInsertion = (e) => {
setOpenModalFolder(true);
};
const handleDeleteFolder = (e) => {
setLoading(true);
setError(false);
deleteFolder(formValues);
};
const handleFolderFormSubmit = () => {
setOpenModalFolder(false);
setOpenModalInstallation(false);
};
const handleInstallationFormSubmit = () => {
setOpenModalFolder(false);
setOpenModalInstallation(false);
};
const handleFormCancel = () => {
setOpenModalFolder(false);
setOpenModalInstallation(false);
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!formValues[field]) {
return false;
}
}
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
}}
>
{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'
}}
/>
)}
{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' }}
>
<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' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
</div>
</Box>
</CardContent>
</Grid>
</Grid>
</Container>
)}
{currentTab === 'manage' && currentUser.hasWriteAccess && (
<AccessContextProvider>
<Access
currentResource={formValues}
resourceType="folder"
></Access>
</AccessContextProvider>
)}
</Grid>
</Card>
</Grid>
</>
);
}
export default Folder;

View File

@ -0,0 +1,151 @@
import React, { useContext, useEffect, useState } from 'react';
import { Grid, useTheme } 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';
function InstallationTree() {
const theme = useTheme();
const { data, 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) => {
return (
subnode != node &&
subnode.parentId == node.id && (
<TreeNode
key={
subnode.id.toString() +
parent_id.toString() +
subnode.type
}
node={subnode}
parent_id={node.id}
/>
)
);
})}
</CustomTreeItem>
)
);
} else {
const status = getStatus(node.id);
return (
node.parentId == parent_id && (
<CustomTreeItem
node={node}
parent_id={parent_id}
handleSelectedInstallation={() => handleSelectOneInstallation(node)}
status={status}
/>
)
);
}
};
return (
<Grid container spacing={1} sx={{ marginTop: 0.1 }}>
<Grid item xs={12} md={3}>
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
>
{data.map((node, index) => {
return (
<TreeNode
key={node.id.toString() + node.parentId.toString() + node.type}
node={node}
parent_id={'0'}
/>
);
})}
</TreeView>
</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>
);
}
export default InstallationTree;

View File

@ -0,0 +1,182 @@
import React, { useContext, useState } from 'react';
import {
Alert,
Box,
CircularProgress,
IconButton,
Modal,
TextField,
useTheme
} from '@mui/material';
import Button from '@mui/material/Button';
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';
interface folderFormProps {
cancel: () => void;
submit: () => void;
parentid: number;
}
function folderForm(props: folderFormProps) {
const theme = useTheme();
const [open, setOpen] = useState(true);
const [formValues, setFormValues] = useState<Partial<I_Folder>>({
name: '',
information: ''
});
const requiredFields = ['name'];
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const installationContext = useContext(InstallationsContext);
const { loading, setLoading, error, setError, createFolder } =
installationContext;
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSubmit = async (e) => {
formValues.parentId = props.parentid;
setLoading(true);
const responseData = await createFolder(formValues);
props.submit();
};
const handleCancelSubmit = (e) => {
props.cancel();
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!formValues[field]) {
return false;
}
}
return true;
};
return (
<>
<Modal
open={open}
onClose={() => {}}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '30%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 600,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4
}}
>
<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}
fullWidth
/>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
variant="contained"
onClick={handleSubmit}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
Submit
</Button>
<Button
variant="contained"
onClick={handleCancelSubmit}
sx={{
marginLeft: '10px'
}}
>
Cancel
</Button>
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
/>
)}
{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' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
</div>
</Box>
</Box>
</Modal>
</>
);
}
export default folderForm;

View File

@ -0,0 +1,272 @@
import React, { ChangeEvent, useContext, useEffect, useState } from 'react';
import {
Alert,
Box,
Card,
CardContent,
CircularProgress,
Container,
Grid,
IconButton,
Tab,
Tabs,
TextField,
useTheme
} from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material'; // Import CloseIcon
import Button from '@mui/material/Button';
import axiosConfig from 'src/Resources/axiosConfig';
import { InnovEnergyUser } from 'src/interfaces/UserTypes';
import { TokenContext } from 'src/contexts/tokenContext';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
interface singleUserProps {
current_user: InnovEnergyUser;
fetchDataAgain: () => void;
}
function User(props: singleUserProps) {
const theme = useTheme();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [updated, setUpdated] = useState(false);
const [currentTab, setCurrentTab] = useState<string>('user');
const [formValues, setFormValues] = useState(props.current_user);
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
useEffect(() => {
setFormValues(props.current_user);
}, [props.current_user]);
if (formValues == undefined) {
return null;
}
const tabs = [{ value: 'user', label: 'User' }];
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value);
setError(false);
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSubmit = (e) => {
setLoading(true);
setError(false);
axiosConfig
.put('/UpdateUser', formValues)
.then((response) => {
if (response) {
props.fetchDataAgain();
setLoading(false);
setUpdated(true);
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
};
const handleDelete = (e) => {
setLoading(true);
setError(false);
axiosConfig
.delete(`/DeleteUser?userId=${formValues.id}`)
.then((response) => {
if (response) {
props.fetchDataAgain();
setLoading(false);
setUpdated(true);
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
};
return (
<>
<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 === 'user' && (
<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
/>
</div>
<div>
<TextField
label="Email"
name="email"
value={formValues.email}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label="Information"
name="information"
value={formValues.information}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
variant="contained"
onClick={handleSubmit}
sx={{
marginLeft: '10px'
}}
>
Apply Changes
</Button>
<Button
variant="contained"
onClick={handleDelete}
sx={{
marginLeft: '10px'
}}
>
Delete User
</Button>
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
/>
)}
{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' }}
>
<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' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
</div>
</Box>
</CardContent>
</Grid>
</Grid>
</Container>
)}
</Grid>
</Card>
</Grid>
</>
);
}
export default User;

View File

@ -0,0 +1,428 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import {
Alert,
Box,
CircularProgress,
FormControl,
IconButton,
InputLabel,
MenuItem,
Modal,
Select,
TextField,
useTheme
} from '@mui/material';
import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material';
import { InnovEnergyUser } from 'src/interfaces/UserTypes';
import axiosConfig from 'src/Resources/axiosConfig';
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';
interface userFormProps {
cancel: () => void;
submit: () => void;
}
function userForm(props: userFormProps) {
const theme = useTheme();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [errormessage, setErrorMessage] = useState('An error has occured');
const [openInstallation, setOpenInstallation] = useState(false);
const [openFolder, setOpenFolder] = useState(false);
const [formValues, setFormValues] = useState<Partial<InnovEnergyUser>>({
name: '',
email: '',
information: ''
});
const requiredFields = ['name', 'email'];
const [selectedFolderNames, setSelectedFolderNames] = useState<string[]>([]);
const [selectedInstallationNames, setSelectedInstallationNames] = useState<
string[]
>([]);
const [folders, setFolders] = useState<I_Folder[]>([]);
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const [installations, setInstallations] = useState<I_Installation[]>([]);
const fetchFolders = useCallback(async () => {
setLoading(true);
return axiosConfig
.get('/GetAllFolders')
.then((res) => {
setFolders(res.data);
setLoading(false);
})
.catch((err) => {
setLoading(false);
if (err.response && err.response.status == 401) {
removeToken();
}
});
}, [setFolders]);
const fetchInstallations = useCallback(async () => {
setLoading(true);
return axiosConfig
.get('/GetAllInstallations')
.then((res) => {
setInstallations(res.data);
setLoading(false);
})
.catch((err) => {
setLoading(false);
if (err.response && err.response.status == 401) {
removeToken();
}
});
}, [setInstallations]);
useEffect(() => {
fetchFolders();
fetchInstallations();
}, []);
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleFolderChange = (event) => {
setSelectedFolderNames(event.target.value);
};
const handleInstallationChange = (event) => {
setSelectedInstallationNames(event.target.value);
};
const handleSubmit = async (e) => {
const res = await axiosConfig.post('/CreateUser', {
...formValues,
password: '',
language: 'english'
});
try {
for (const folderName of selectedFolderNames) {
const folder = folders.find((folder) => folder.name === folderName);
await axiosConfig.post(
`/GrantUserAccessToFolder?UserId=${res.data.id}&FolderId=${folder.id}`
);
}
for (const installationName of selectedInstallationNames) {
const installation = installations.find(
(installation) => installation.name === installationName
);
await axiosConfig.post(
`/GrantUserAccessToInstallation?UserId=${res.data.id}&InstallationId=${installation.id}`
);
}
setLoading(false);
props.submit();
} catch (error) {
await axiosConfig
.delete(`/DeleteUser?userId=${res.data.id}`)
.then((response) => {
setLoading(false);
setErrorMessage('An error has occured');
setError(true);
setTimeout(() => {
props.cancel();
}, 2000);
});
}
};
const handleCancelSubmit = (e) => {
props.cancel();
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!formValues[field]) {
return false;
}
}
return true;
};
const handleAdminChange = (event) => {
formValues.hasWriteAccess = event.target.checked;
};
const handleOpenInstallation = () => {
setOpenInstallation(true);
};
const handleCloseInstallation = () => {
setOpenInstallation(false);
};
const handleOpenFolder = () => {
setOpenFolder(true);
};
const handleCloseFolder = () => {
setOpenFolder(false);
};
return (
<>
<Modal
open={true}
onClose={() => {}}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '30%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 600,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4
}}
>
<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="Email"
name="email"
value={formValues.email}
onChange={handleChange}
fullWidth
required
error={formValues.email === ''}
/>
</div>
<div>
<TextField
label="Information"
name="information"
value={formValues.information}
onChange={handleChange}
fullWidth
/>
</div>
<div>
<FormControl
fullWidth
sx={{ marginLeft: 1, width: 390, marginTop: 1 }}
>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'transparent'
}}
>
Grant access to folders
</InputLabel>
<Select
multiple
value={selectedFolderNames}
onChange={handleFolderChange}
open={openFolder}
onClose={handleCloseFolder}
onOpen={handleOpenFolder}
renderValue={(selected) => (
<div>
{selected.map((folder) => (
<span key={folder}>{folder}, </span>
))}
</div>
)}
>
{folders.map((folder) => (
<MenuItem key={folder.id} value={folder.name}>
{folder.name}
</MenuItem>
))}
<Button
sx={{
marginLeft: '150px',
marginTop: '10px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
onClick={handleCloseFolder}
>
Submit
</Button>
</Select>
</FormControl>
</div>
<div
style={{
display: 'flex'
}}
>
<FormControl
fullWidth
sx={{ marginLeft: 1, width: 390, marginTop: 2 }}
>
<InputLabel
sx={{
fontSize: 14
}}
>
Grant access to installations
</InputLabel>
<Select
multiple
value={selectedInstallationNames}
onChange={handleInstallationChange}
open={openInstallation}
onClose={handleCloseInstallation}
onOpen={handleOpenInstallation}
renderValue={(selected) => (
<div>
{selected.map((installation) => (
<span key={installation}>{installation}, </span>
))}
</div>
)}
>
{installations.map((installation) => (
<MenuItem key={installation.id} value={installation.name}>
{installation.name}
</MenuItem>
))}
<Button
sx={{
marginLeft: '150px',
marginTop: '10px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
onClick={handleCloseInstallation}
>
Submit
</Button>
</Select>
</FormControl>
</div>
<div>
<FormControlLabel
control={
<Checkbox
onChange={handleAdminChange}
sx={{
marginLeft: '11px'
}}
/>
}
label="Admin"
sx={{ marginTop: 1 }}
/>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
variant="contained"
onClick={handleSubmit}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
Submit
</Button>
<Button
variant="contained"
onClick={handleCancelSubmit}
sx={{
marginLeft: '10px'
}}
>
Cancel
</Button>
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
/>
)}
{error && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
{errormessage}
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)} // Set error state to false on click
sx={{ marginLeft: '4px' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
</div>
</Box>
</Box>
</Modal>
</>
);
}
export default userForm;

View File

@ -0,0 +1,97 @@
import { Helmet } from 'react-helmet-async';
import PageTitle from 'src/components/PageTitle';
import PageTitleWrapper from 'src/components/PageTitleWrapper';
import {
Container,
Grid,
Card,
CardHeader,
CardContent,
Divider
} from '@mui/material';
import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Footer from 'src/components/Footer';
function Accordions() {
return (
<>
<Helmet>
<title>Accordions - Components</title>
</Helmet>
<PageTitleWrapper>
<PageTitle
heading="Accordions"
subHeading="Accordions contain creation flows and allow lightweight editing of an element."
docs="https://material-ui.com/components/accordion/"
/>
</PageTitleWrapper>
<Container maxWidth="lg">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12}>
<Card>
<CardHeader title="Basic Example" />
<Divider />
<CardContent>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
>
<Typography>Accordion 1</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Suspendisse malesuada lacus ex, sit amet blandit leo
lobortis eget.
</Typography>
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel2a-content"
id="panel2a-header"
>
<Typography>Accordion 2</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Suspendisse malesuada lacus ex, sit amet blandit leo
lobortis eget.
</Typography>
</AccordionDetails>
</Accordion>
<Accordion disabled>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel3a-content"
id="panel3a-header"
>
<Typography>Disabled Accordion</Typography>
</AccordionSummary>
</Accordion>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
<Footer />
</>
);
}
export default Accordions;

View File

@ -0,0 +1,164 @@
import { Helmet } from 'react-helmet-async';
import PageTitle from 'src/components/PageTitle';
import PageTitleWrapper from 'src/components/PageTitleWrapper';
import {
Container,
Grid,
Card,
CardHeader,
CardContent,
Divider
} from '@mui/material';
import Footer from 'src/components/Footer';
import Avatar from '@mui/material/Avatar';
import Stack from '@mui/material/Stack';
import { deepOrange, deepPurple, green, pink } from '@mui/material/colors';
import FolderIcon from '@mui/icons-material/Folder';
import PageviewIcon from '@mui/icons-material/Pageview';
import AssignmentIcon from '@mui/icons-material/Assignment';
function stringToColor(string: string) {
let hash = 0;
let i;
/* eslint-disable no-bitwise */
for (i = 0; i < string.length; i += 1) {
hash = string.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
for (i = 0; i < 3; i += 1) {
const value = (hash >> (i * 8)) & 0xff;
color += `00${value.toString(16)}`.substr(-2);
}
/* eslint-enable no-bitwise */
return color;
}
function stringAvatar(name: string) {
return {
sx: {
bgcolor: stringToColor(name)
},
children: `${name.split(' ')[0][0]}${name.split(' ')[1][0]}`
};
}
function Avatars() {
return (
<>
<Helmet>
<title>Avatars - Components</title>
</Helmet>
<PageTitleWrapper>
<PageTitle
heading="Avatars"
subHeading="Avatars are found throughout material design with uses in everything from tables to dialog menus."
docs="https://material-ui.com/components/avatars/"
/>
</PageTitleWrapper>
<Container maxWidth="lg">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12}>
<Card>
<CardHeader title="Images" />
<Divider />
<CardContent>
<Stack direction="row" spacing={2}>
<Avatar alt="Remy Sharp" src="/static/images/avatars/1.jpg" />
<Avatar
alt="Travis Howard"
src="/static/images/avatars/2.jpg"
/>
<Avatar
alt="Cindy Baker"
src="/static/images/avatars/3.jpg"
/>
</Stack>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Letters" />
<Divider />
<CardContent>
<Stack direction="row" spacing={2}>
<Avatar>H</Avatar>
<Avatar sx={{ bgcolor: deepOrange[500] }}>N</Avatar>
<Avatar sx={{ bgcolor: deepPurple[500] }}>OP</Avatar>
</Stack>
<Divider sx={{ my: 5 }} />
<Stack direction="row" spacing={2}>
<Avatar {...stringAvatar('Kent Dodds')} />
<Avatar {...stringAvatar('Jed Watson')} />
<Avatar {...stringAvatar('Tim Neutkens')} />
</Stack>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Sizes" />
<Divider />
<CardContent>
<Stack direction="row" spacing={2}>
<Avatar
alt="Remy Sharp"
src="/static/images/avatars/4.jpg"
sx={{ width: 24, height: 24 }}
/>
<Avatar alt="Remy Sharp" src="/static/images/avatars/5.jpg" />
<Avatar
alt="Remy Sharp"
src="/static/images/avatars/3.jpg"
sx={{ width: 56, height: 56 }}
/>
</Stack>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Icons" />
<Divider />
<CardContent>
<Stack direction="row" spacing={2}>
<Avatar>
<FolderIcon />
</Avatar>
<Avatar sx={{ bgcolor: pink[500] }}>
<PageviewIcon />
</Avatar>
<Avatar sx={{ bgcolor: green[500] }}>
<AssignmentIcon />
</Avatar>
</Stack>
<Divider sx={{ my: 5 }} />
<Stack direction="row" spacing={2}>
<Avatar sx={{ bgcolor: deepOrange[500] }} variant="square">
N
</Avatar>
<Avatar sx={{ bgcolor: green[500] }} variant="rounded">
<AssignmentIcon />
</Avatar>
</Stack>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
<Footer />
</>
);
}
export default Avatars;

View File

@ -0,0 +1,176 @@
import { Helmet } from 'react-helmet-async';
import PageTitle from 'src/components/PageTitle';
import PageTitleWrapper from 'src/components/PageTitleWrapper';
import {
Container,
Grid,
Card,
CardHeader,
CardContent,
Divider
} from '@mui/material';
import { useState } from 'react';
import Footer from 'src/components/Footer';
import ButtonGroup from '@mui/material/ButtonGroup';
import Button from '@mui/material/Button';
import AddIcon from '@mui/icons-material/Add';
import RemoveIcon from '@mui/icons-material/Remove';
import MailIcon from '@mui/icons-material/Mail';
import Switch from '@mui/material/Switch';
import FormControlLabel from '@mui/material/FormControlLabel';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Badge from '@mui/material/Badge';
const shapeStyles = { bgcolor: 'primary.main', width: 40, height: 40 };
const shapeCircleStyles = { borderRadius: '50%' };
const rectangle = <Box component="span" sx={shapeStyles} />;
const circle = (
<Box component="span" sx={{ ...shapeStyles, ...shapeCircleStyles }} />
);
function Badges() {
const [count, setCount] = useState(1);
const [invisible, setInvisible] = useState(false);
const handleBadgeVisibility = () => {
setInvisible(!invisible);
};
return (
<>
<Helmet>
<title>Badges - Components</title>
</Helmet>
<PageTitleWrapper>
<PageTitle
heading="Badges"
subHeading="Badge generates a small badge to the top-right of its child(ren)."
docs="https://material-ui.com/components/badges/"
/>
</PageTitleWrapper>
<Container maxWidth="lg">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12}>
<Card>
<CardHeader title="Shapes" />
<Divider />
<CardContent>
<Stack spacing={3} direction="row">
<Badge color="secondary" badgeContent=" ">
{rectangle}
</Badge>
<Badge color="secondary" badgeContent=" " variant="dot">
{rectangle}
</Badge>
<Badge color="secondary" overlap="circular" badgeContent=" ">
{circle}
</Badge>
<Badge
color="secondary"
overlap="circular"
badgeContent=" "
variant="dot"
>
{circle}
</Badge>
</Stack>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Badges Visibility" />
<Divider />
<CardContent>
<Box
sx={{
color: 'action.active',
display: 'flex',
flexDirection: 'column',
'& > *': {
marginBottom: 2
},
'& .MuiBadge-root': {
marginRight: 4
}
}}
>
<div>
<Badge color="secondary" badgeContent={count}>
<MailIcon />
</Badge>
<ButtonGroup>
<Button
aria-label="reduce"
onClick={() => {
setCount(Math.max(count - 1, 0));
}}
>
<RemoveIcon fontSize="small" />
</Button>
<Button
aria-label="increase"
onClick={() => {
setCount(count + 1);
}}
>
<AddIcon fontSize="small" />
</Button>
</ButtonGroup>
</div>
<div>
<Badge
color="secondary"
variant="dot"
invisible={invisible}
>
<MailIcon />
</Badge>
<FormControlLabel
sx={{ color: 'text.primary' }}
control={
<Switch
checked={!invisible}
onChange={handleBadgeVisibility}
/>
}
label="Show Badge"
/>
</div>
</Box>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Colors" />
<Divider />
<CardContent>
<Stack spacing={2} direction="row">
<Badge badgeContent={4} color="secondary">
<MailIcon color="action" />
</Badge>
<Badge badgeContent={4} color="success">
<MailIcon color="action" />
</Badge>
</Stack>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
<Footer />
</>
);
}
export default Badges;

View File

@ -0,0 +1,218 @@
import { Helmet } from 'react-helmet-async';
import PageTitle from 'src/components/PageTitle';
import PageTitleWrapper from 'src/components/PageTitleWrapper';
import {
Button,
Container,
IconButton,
Grid,
Card,
CardHeader,
CardContent,
Divider
} from '@mui/material';
import Footer from 'src/components/Footer';
import DeleteIcon from '@mui/icons-material/Delete';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
function Buttons() {
return (
<>
<Helmet>
<title>Buttons - Components</title>
</Helmet>
<PageTitleWrapper>
<PageTitle
heading="Buttons"
subHeading="Buttons allow users to take actions, and make choices, with a single tap."
docs="https://material-ui.com/components/buttons/"
/>
</PageTitleWrapper>
<Container maxWidth="lg">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12}>
<Card>
<CardHeader title="Contained Buttons" />
<Divider />
<CardContent>
<Button sx={{ margin: 1 }} variant="contained">
Default
</Button>
<Button sx={{ margin: 1 }} variant="contained" color="primary">
Primary
</Button>
<Button
sx={{ margin: 1 }}
variant="contained"
color="secondary"
>
Secondary
</Button>
<Button sx={{ margin: 1 }} variant="contained" disabled>
Disabled
</Button>
<Button
sx={{ margin: 1 }}
variant="contained"
color="primary"
href="#contained-buttons"
>
Link
</Button>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Text Buttons" />
<Divider />
<CardContent>
<Button sx={{ margin: 1 }}>Default</Button>
<Button sx={{ margin: 1 }} color="primary">
Primary
</Button>
<Button sx={{ margin: 1 }} color="secondary">
Secondary
</Button>
<Button sx={{ margin: 1 }} disabled>
Disabled
</Button>
<Button sx={{ margin: 1 }} href="#text-buttons" color="primary">
Link
</Button>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Outlined Buttons" />
<Divider />
<CardContent>
<Button variant="outlined" sx={{ margin: 1 }}>
Default
</Button>
<Button variant="outlined" sx={{ margin: 1 }} color="primary">
Primary
</Button>
<Button variant="outlined" sx={{ margin: 1 }} color="secondary">
Secondary
</Button>
<Button variant="outlined" sx={{ margin: 1 }} disabled>
Disabled
</Button>
<Button
variant="outlined"
sx={{ margin: 1 }}
color="primary"
href="#outlined-buttons"
>
Link
</Button>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Sizes" />
<Divider />
<CardContent>
<div>
<div>
<Button size="small" sx={{ margin: 1 }}>
Small
</Button>
<Button size="medium" sx={{ margin: 1 }}>
Medium
</Button>
<Button size="large" sx={{ margin: 1 }}>
Large
</Button>
</div>
<div>
<Button
variant="outlined"
sx={{ margin: 1 }}
size="small"
color="primary"
>
Small
</Button>
<Button
variant="outlined"
sx={{ margin: 1 }}
size="medium"
color="primary"
>
Medium
</Button>
<Button
variant="outlined"
sx={{ margin: 1 }}
size="large"
color="primary"
>
Large
</Button>
</div>
<div>
<Button
sx={{ margin: 1 }}
variant="contained"
size="small"
color="primary"
>
Small
</Button>
<Button
sx={{ margin: 1 }}
variant="contained"
size="medium"
color="primary"
>
Medium
</Button>
<Button
sx={{ margin: 1 }}
variant="contained"
size="large"
color="primary"
>
Large
</Button>
</div>
<div>
<IconButton
aria-label="delete"
sx={{ margin: 1 }}
size="small"
>
<ArrowDownwardIcon fontSize="inherit" />
</IconButton>
<IconButton aria-label="delete" sx={{ margin: 1 }}>
<DeleteIcon fontSize="small" />
</IconButton>
<IconButton aria-label="delete" sx={{ margin: 1 }}>
<DeleteIcon />
</IconButton>
<IconButton aria-label="delete" sx={{ margin: 1 }}>
<DeleteIcon fontSize="large" />
</IconButton>
</div>
</div>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
<Footer />
</>
);
}
export default Buttons;

View File

@ -0,0 +1,241 @@
import { Helmet } from 'react-helmet-async';
import { useState } from 'react';
import PageTitle from 'src/components/PageTitle';
import PageTitleWrapper from 'src/components/PageTitleWrapper';
import {
Container,
Grid,
Card,
CardHeader,
CardContent,
Divider
} from '@mui/material';
import Footer from 'src/components/Footer';
import CardActions from '@mui/material/CardActions';
import { styled } from '@mui/material/styles';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import CardMedia from '@mui/material/CardMedia';
import Collapse from '@mui/material/Collapse';
import Avatar from '@mui/material/Avatar';
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { red } from '@mui/material/colors';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import MoreVertIcon from '@mui/icons-material/MoreVert';
interface ExpandMoreProps extends IconButtonProps {
expand: boolean;
}
const ExpandMore = styled((props: ExpandMoreProps) => {
const { expand, ...other } = props;
return <IconButton {...other} />;
})(({ theme, expand }) => ({
transform: !expand ? 'rotate(0deg)' : 'rotate(180deg)',
marginLeft: 'auto',
transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest
})
}));
const bull = (
<Box
component="span"
sx={{ display: 'inline-block', mx: '2px', transform: 'scale(0.8)' }}
>
</Box>
);
function Cards() {
const [expanded, setExpanded] = useState(false);
const handleExpandClick = () => {
setExpanded(!expanded);
};
return (
<>
<Helmet>
<title>Cards - Components</title>
</Helmet>
<PageTitleWrapper>
<PageTitle
heading="Cards"
subHeading="Cards contain content and actions about a single subject."
docs="https://material-ui.com/components/cards/"
/>
</PageTitleWrapper>
<Container maxWidth="lg">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12}>
<Card>
<CardHeader title="Basic" />
<Divider />
<CardContent>
<Card sx={{ minWidth: 275 }}>
<CardContent>
<Typography
sx={{ fontSize: 14 }}
color="text.secondary"
gutterBottom
>
Word of the Day
</Typography>
<Typography variant="h5" component="div">
be{bull}nev{bull}o{bull}lent
</Typography>
<Typography sx={{ mb: 1.5 }} color="text.secondary">
adjective
</Typography>
<Typography variant="body2">
well meaning and kindly.
<br />
{'"a benevolent smile"'}
</Typography>
</CardContent>
<CardActions>
<Button size="small">Learn More</Button>
</CardActions>
</Card>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Complex Example" />
<Divider />
<CardContent>
<Card sx={{ maxWidth: 345 }}>
<CardHeader
avatar={
<Avatar sx={{ bgcolor: red[500] }} aria-label="recipe">
R
</Avatar>
}
action={
<IconButton aria-label="settings">
<MoreVertIcon />
</IconButton>
}
title="Shrimp and Chorizo Paella"
subheader="September 14, 2016"
/>
<CardMedia
sx={{
height: 0,
paddingTop: '56.25%' // 16:9
}}
image="/static/images/placeholders/covers/1.jpg"
title="Paella dish"
/>
<CardContent>
<Typography variant="body2" color="text.secondary">
This impressive paella is a perfect party dish and a fun
meal to cook together with your guests. Add 1 cup of
frozen peas along with the mussels, if you like.
</Typography>
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add to favorites">
<FavoriteIcon />
</IconButton>
<IconButton aria-label="share">
<ShareIcon />
</IconButton>
<ExpandMore
expand={expanded}
onClick={handleExpandClick}
aria-expanded={expanded}
aria-label="show more"
>
<ExpandMoreIcon />
</ExpandMore>
</CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent>
<Typography paragraph>Method:</Typography>
<Typography paragraph>
Heat 1/2 cup of the broth in a pot until simmering, add
saffron and set aside for 10 minutes.
</Typography>
<Typography paragraph>
Heat oil in a (14- to 16-inch) paella pan or a large,
deep skillet over medium-high heat. Add chicken, shrimp
and chorizo, and cook, stirring occasionally until
lightly browned, 6 to 8 minutes. Transfer shrimp to a
large plate and set aside, leaving chicken and chorizo
in the pan. Add pimentón, bay leaves, garlic, tomatoes,
onion, salt and pepper, and cook, stirring often until
thickened and fragrant, about 10 minutes. Add saffron
broth and remaining 4 1/2 cups chicken broth; bring to a
boil.
</Typography>
<Typography paragraph>
Add rice and stir very gently to distribute. Top with
artichokes and peppers, and cook without stirring, until
most of the liquid is absorbed, 15 to 18 minutes. Reduce
heat to medium-low, add reserved shrimp and mussels,
tucking them down into the rice, and cook again without
stirring, until mussels have opened and rice is just
tender, 5 to 7 minutes more. (Discard any mussels that
dont open.)
</Typography>
<Typography>
Set aside off of the heat to let rest for 10 minutes,
and then serve.
</Typography>
</CardContent>
</Collapse>
</Card>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Media" />
<Divider />
<CardContent>
<Card sx={{ maxWidth: 345 }}>
<CardMedia
sx={{ height: 140 }}
image="/static/images/placeholders/covers/6.jpg"
title="Contemplative Reptile"
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Lizard
</Typography>
<Typography variant="body2" color="text.secondary">
Lizards are a widespread group of squamate reptiles, with
over 6,000 species, ranging across all continents except
Antarctica
</Typography>
</CardContent>
<CardActions>
<Button size="small">Share</Button>
<Button size="small">Learn More</Button>
</CardActions>
</Card>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
<Footer />
</>
);
}
export default Cards;

View File

@ -0,0 +1,482 @@
import { Helmet } from 'react-helmet-async';
import PageTitle from 'src/components/PageTitle';
import { useState } from 'react';
import PageTitleWrapper from 'src/components/PageTitleWrapper';
import {
Container,
Grid,
Card,
CardHeader,
CardContent,
Divider
} from '@mui/material';
import Footer from 'src/components/Footer';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import MenuItem from '@mui/material/MenuItem';
import { pink } from '@mui/material/colors';
import Checkbox from '@mui/material/Checkbox';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
import Stack from '@mui/material/Stack';
import Slider from '@mui/material/Slider';
import VolumeDown from '@mui/icons-material/VolumeDown';
import VolumeUp from '@mui/icons-material/VolumeUp';
import Switch from '@mui/material/Switch';
const label = { inputProps: { 'aria-label': 'Switch demo' } };
const currencies = [
{
value: 'USD',
label: '$'
},
{
value: 'EUR',
label: '€'
},
{
value: 'BTC',
label: '฿'
},
{
value: 'JPY',
label: '¥'
}
];
function Forms() {
const [currency, setCurrency] = useState('EUR');
const handleChange = (event) => {
setCurrency(event.target.value);
};
const [value, setValue] = useState(30);
const handleChange2 = (event, newValue) => {
setValue(newValue);
};
return (
<>
<Helmet>
<title>Forms - Components</title>
</Helmet>
<PageTitleWrapper>
<PageTitle
heading="Forms"
subHeading="Components that are used to build interactive placeholders used for data collection from users."
docs="https://material-ui.com/components/text-fields/"
/>
</PageTitleWrapper>
<Container maxWidth="lg">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12}>
<Card>
<CardHeader title="Input Fields" />
<Divider />
<CardContent>
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '25ch' }
}}
noValidate
autoComplete="off"
>
<div>
<TextField
required
id="outlined-required"
label="Required"
defaultValue="Hello World"
/>
<TextField
disabled
id="outlined-disabled"
label="Disabled"
defaultValue="Hello World"
/>
<TextField
id="outlined-password-input"
label="Password"
type="password"
autoComplete="current-password"
/>
<TextField
id="outlined-read-only-input"
label="Read Only"
defaultValue="Hello World"
InputProps={{
readOnly: true
}}
/>
<TextField
id="outlined-number"
label="Number"
type="number"
InputLabelProps={{
shrink: true
}}
/>
<TextField
id="outlined-search"
label="Search field"
type="search"
/>
<TextField
id="outlined-helperText"
label="Helper text"
defaultValue="Default Value"
helperText="Some important text"
/>
</div>
<div>
<TextField
required
id="filled-required"
label="Required"
defaultValue="Hello World"
variant="filled"
/>
<TextField
disabled
id="filled-disabled"
label="Disabled"
defaultValue="Hello World"
variant="filled"
/>
<TextField
id="filled-password-input"
label="Password"
type="password"
autoComplete="current-password"
variant="filled"
/>
<TextField
id="filled-read-only-input"
label="Read Only"
defaultValue="Hello World"
InputProps={{
readOnly: true
}}
variant="filled"
/>
<TextField
id="filled-number"
label="Number"
type="number"
InputLabelProps={{
shrink: true
}}
variant="filled"
/>
<TextField
id="filled-search"
label="Search field"
type="search"
variant="filled"
/>
<TextField
id="filled-helperText"
label="Helper text"
defaultValue="Default Value"
helperText="Some important text"
variant="filled"
/>
</div>
<div>
<TextField
required
id="standard-required"
label="Required"
defaultValue="Hello World"
variant="standard"
/>
<TextField
disabled
id="standard-disabled"
label="Disabled"
defaultValue="Hello World"
variant="standard"
/>
<TextField
id="standard-password-input"
label="Password"
type="password"
autoComplete="current-password"
variant="standard"
/>
<TextField
id="standard-read-only-input"
label="Read Only"
defaultValue="Hello World"
InputProps={{
readOnly: true
}}
variant="standard"
/>
<TextField
id="standard-number"
label="Number"
type="number"
InputLabelProps={{
shrink: true
}}
variant="standard"
/>
<TextField
id="standard-search"
label="Search field"
type="search"
variant="standard"
/>
<TextField
id="standard-helperText"
label="Helper text"
defaultValue="Default Value"
helperText="Some important text"
variant="standard"
/>
</div>
</Box>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Select Inputs" />
<Divider />
<CardContent>
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '25ch' }
}}
noValidate
autoComplete="off"
>
<div>
<TextField
id="outlined-select-currency"
select
label="Select"
value={currency}
onChange={handleChange}
helperText="Please select your currency"
>
{currencies.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<TextField
id="outlined-select-currency-native"
select
label="Native select"
value={currency}
onChange={handleChange}
SelectProps={{
native: true
}}
helperText="Please select your currency"
>
{currencies.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</TextField>
</div>
<div>
<TextField
id="filled-select-currency"
select
label="Select"
value={currency}
onChange={handleChange}
helperText="Please select your currency"
variant="filled"
>
{currencies.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<TextField
id="filled-select-currency-native"
select
label="Native select"
value={currency}
onChange={handleChange}
SelectProps={{
native: true
}}
helperText="Please select your currency"
variant="filled"
>
{currencies.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</TextField>
</div>
<div>
<TextField
id="standard-select-currency"
select
label="Select"
value={currency}
onChange={handleChange}
helperText="Please select your currency"
variant="standard"
>
{currencies.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<TextField
id="standard-select-currency-native"
select
label="Native select"
value={currency}
onChange={handleChange}
SelectProps={{
native: true
}}
helperText="Please select your currency"
variant="standard"
>
{currencies.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</TextField>
</div>
</Box>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Switches" />
<Divider />
<CardContent>
<Switch {...label} defaultChecked />
<Switch {...label} />
<Switch {...label} disabled defaultChecked />
<Switch {...label} disabled />
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Checkboxes &amp; Radios" />
<Divider />
<CardContent>
<Checkbox {...label} defaultChecked />
<Checkbox {...label} defaultChecked color="secondary" />
<Checkbox {...label} defaultChecked color="success" />
<Checkbox {...label} defaultChecked color="default" />
<Checkbox
{...label}
defaultChecked
sx={{
color: pink[800],
'&.Mui-checked': {
color: pink[600]
}
}}
/>
<Divider sx={{ my: 5 }} />
<FormControl component="fieldset">
<FormLabel component="legend">Gender</FormLabel>
<RadioGroup
row
aria-label="gender"
name="row-radio-buttons-group"
>
<FormControlLabel
value="female"
control={<Radio />}
label="Female"
/>
<FormControlLabel
value="male"
control={<Radio />}
label="Male"
/>
<FormControlLabel
value="other"
control={<Radio />}
label="Other"
/>
<FormControlLabel
value="disabled"
disabled
control={<Radio />}
label="other"
/>
</RadioGroup>
</FormControl>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardHeader title="Sliders" />
<Divider />
<CardContent>
<Box sx={{ width: 200 }}>
<Stack
spacing={2}
direction="row"
sx={{ mb: 1 }}
alignItems="center"
>
<VolumeDown />
<Slider
aria-label="Volume"
value={value}
onChange={handleChange2}
/>
<VolumeUp />
</Stack>
<Slider
disabled
defaultValue={30}
aria-label="Disabled slider"
/>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
<Footer />
</>
);
}
export default Forms;

View File

@ -0,0 +1,144 @@
import { Helmet } from 'react-helmet-async';
import PropTypes from 'prop-types';
import { useState } from 'react';
import PageTitle from 'src/components/PageTitle';
import PageTitleWrapper from 'src/components/PageTitleWrapper';
import {
Container,
Grid,
Card,
CardHeader,
CardContent,
Divider
} from '@mui/material';
import Button from '@mui/material/Button';
import Avatar from '@mui/material/Avatar';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import ListItemText from '@mui/material/ListItemText';
import DialogTitle from '@mui/material/DialogTitle';
import Dialog from '@mui/material/Dialog';
import PersonIcon from '@mui/icons-material/Person';
import AddIcon from '@mui/icons-material/Add';
import Typography from '@mui/material/Typography';
import { blue } from '@mui/material/colors';
import Footer from 'src/components/Footer';
const emails = ['username@gmail.com', 'user02@gmail.com'];
function SimpleDialog(props) {
const { onClose, selectedValue, open } = props;
const handleClose = () => {
onClose(selectedValue);
};
const handleListItemClick = (value) => {
onClose(value);
};
return (
<Dialog onClose={handleClose} open={open}>
<DialogTitle>Set backup account</DialogTitle>
<List sx={{ pt: 0 }}>
{emails.map((email) => (
<ListItem
button
onClick={() => handleListItemClick(email)}
key={email}
>
<ListItemAvatar>
<Avatar sx={{ bgcolor: blue[100], color: blue[600] }}>
<PersonIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={email} />
</ListItem>
))}
<ListItem
autoFocus
button
onClick={() => handleListItemClick('addAccount')}
>
<ListItemAvatar>
<Avatar>
<AddIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Add account" />
</ListItem>
</List>
</Dialog>
);
}
SimpleDialog.propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
selectedValue: PropTypes.string.isRequired
};
function Modals() {
const [open, setOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState(emails[1]);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = (value) => {
setOpen(false);
setSelectedValue(value);
};
return (
<>
<Helmet>
<title>Modals - Components</title>
</Helmet>
<PageTitleWrapper>
<PageTitle
heading="Modals"
subHeading="Dialogs inform users about a task and can contain critical information, require decisions, or involve multiple tasks."
docs="https://material-ui.com/components/dialogs/"
/>
</PageTitleWrapper>
<Container maxWidth="lg">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12}>
<Card>
<CardHeader title="Basic Dialog" />
<Divider />
<CardContent>
<Typography variant="subtitle1" component="div">
Selected: {selectedValue}
</Typography>
<br />
<Button variant="outlined" onClick={handleClickOpen}>
Open simple dialog
</Button>
<SimpleDialog
selectedValue={selectedValue}
open={open}
onClose={handleClose}
/>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
<Footer />
</>
);
}
export default Modals;

View File

@ -0,0 +1,119 @@
import { Helmet } from 'react-helmet-async';
import PageTitle from 'src/components/PageTitle';
import PageTitleWrapper from 'src/components/PageTitleWrapper';
import {
Container,
Grid,
Card,
CardHeader,
CardContent,
Divider
} from '@mui/material';
import { useState, SyntheticEvent } from 'react';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Footer from 'src/components/Footer';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`
};
}
function TabsDemo() {
const [value, setValue] = useState(0);
const handleChange = (event: SyntheticEvent, newValue: number) => {
setValue(newValue);
};
return (
<>
<Helmet>
<title>Tabs - Components</title>
</Helmet>
<PageTitleWrapper>
<PageTitle
heading="Tabs"
subHeading="Tabs make it easy to explore and switch between different views."
docs="https://material-ui.com/components/tabs/"
/>
</PageTitleWrapper>
<Container maxWidth="lg">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12}>
<Card>
<CardHeader title="Basic Example" />
<Divider />
<CardContent>
<Box sx={{ width: '100%' }}>
<Tabs
variant="scrollable"
scrollButtons="auto"
textColor="primary"
indicatorColor="primary"
value={value}
onChange={handleChange}
aria-label="basic tabs example"
>
<Tab label="Item One" {...a11yProps(0)} />
<Tab label="Item Two" {...a11yProps(1)} />
<Tab label="Item Three" {...a11yProps(2)} />
</Tabs>
<TabPanel value={value} index={0}>
Item One
</TabPanel>
<TabPanel value={value} index={1}>
Item Two
</TabPanel>
<TabPanel value={value} index={2}>
Item Three
</TabPanel>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
<Footer />
</>
);
}
export default TabsDemo;

View File

@ -0,0 +1,119 @@
import { Helmet } from 'react-helmet-async';
import PageTitle from 'src/components/PageTitle';
import PageTitleWrapper from 'src/components/PageTitleWrapper';
import {
Container,
Grid,
Card,
CardHeader,
CardContent,
Divider
} from '@mui/material';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
import Footer from 'src/components/Footer';
function Tooltips() {
return (
<>
<Helmet>
<title>Tooltips - Components</title>
</Helmet>
<PageTitleWrapper>
<PageTitle
heading="Tooltips"
subHeading="Tooltips display informative text when users hover over, focus on, or tap an element."
docs="https://material-ui.com/components/tooltips/"
/>
</PageTitleWrapper>
<Container maxWidth="lg">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12}>
<Card>
<CardHeader title="Positioning" />
<Divider />
<CardContent sx={{ display: 'flex', justifyContent: 'center' }}>
<Box sx={{ width: 500 }}>
<Grid container justifyContent="center">
<Grid item>
<Tooltip arrow title="Add" placement="top-start">
<Button>top-start</Button>
</Tooltip>
<Tooltip arrow title="Add" placement="top">
<Button>top</Button>
</Tooltip>
<Tooltip arrow title="Add" placement="top-end">
<Button>top-end</Button>
</Tooltip>
</Grid>
</Grid>
<Grid container justifyContent="center">
<Grid item xs={6}>
<Tooltip arrow title="Add" placement="left-start">
<Button>left-start</Button>
</Tooltip>
<br />
<Tooltip arrow title="Add" placement="left">
<Button>left</Button>
</Tooltip>
<br />
<Tooltip arrow title="Add" placement="left-end">
<Button>left-end</Button>
</Tooltip>
</Grid>
<Grid
item
container
xs={6}
alignItems="flex-end"
direction="column"
>
<Grid item>
<Tooltip arrow title="Add" placement="right-start">
<Button>right-start</Button>
</Tooltip>
</Grid>
<Grid item>
<Tooltip arrow title="Add" placement="right">
<Button>right</Button>
</Tooltip>
</Grid>
<Grid item>
<Tooltip arrow title="Add" placement="right-end">
<Button>right-end</Button>
</Tooltip>
</Grid>
</Grid>
</Grid>
<Grid container justifyContent="center">
<Grid item>
<Tooltip arrow title="Add" placement="bottom-start">
<Button>bottom-start</Button>
</Tooltip>
<Tooltip arrow title="Add" placement="bottom">
<Button>bottom</Button>
</Tooltip>
<Tooltip arrow title="Add" placement="bottom-end">
<Button>bottom-end</Button>
</Tooltip>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Container>
<Footer />
</>
);
}
export default Tooltips;

View File

@ -0,0 +1,183 @@
import { useEffect, useState } from 'react';
import {
Box,
Typography,
Container,
Divider,
OutlinedInput,
IconButton,
Tooltip,
FormControl,
InputAdornment,
Button,
FormHelperText
} from '@mui/material';
import { Helmet } from 'react-helmet-async';
import Logo from 'src/components/LogoSign';
import { styled } from '@mui/material/styles';
import FacebookIcon from '@mui/icons-material/Facebook';
import TwitterIcon from '@mui/icons-material/Twitter';
import InstagramIcon from '@mui/icons-material/Instagram';
import MailTwoToneIcon from '@mui/icons-material/MailTwoTone';
const MainContent = styled(Box)(
() => `
height: 100%;
display: flex;
flex: 1;
overflow: auto;
flex-direction: column;
align-items: center;
justify-content: center;
`
);
const TypographyH1 = styled(Typography)(
({ theme }) => `
font-size: ${theme.typography.pxToRem(75)};
`
);
const TypographyH3 = styled(Typography)(
({ theme }) => `
color: ${theme.colors.alpha.black[50]};
`
);
const OutlinedInputWrapper = styled(OutlinedInput)(
({ theme }) => `
background-color: ${theme.colors.alpha.white[100]};
`
);
const ButtonNotify = styled(Button)(
({ theme }) => `
margin-right: -${theme.spacing(1)};
`
);
function StatusComingSoon() {
const calculateTimeLeft = () => {
const difference = +new Date(`2023`) - +new Date();
let timeLeft = {};
if (difference > 0) {
timeLeft = {
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
minutes: Math.floor((difference / 1000 / 60) % 60),
seconds: Math.floor((difference / 1000) % 60)
};
}
return timeLeft;
};
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft());
useEffect(() => {
setTimeout(() => {
setTimeLeft(calculateTimeLeft());
}, 1000);
});
const timerComponents = [];
Object.keys(timeLeft).forEach((interval) => {
if (!timeLeft[interval]) {
return;
}
timerComponents.push(
<Box textAlign="center" px={3}>
<TypographyH1 variant="h1">{timeLeft[interval]}</TypographyH1>
<TypographyH3 variant="h3">{interval}</TypographyH3>
</Box>
);
});
return (
<>
<Helmet>
<title>Status - Coming Soon</title>
</Helmet>
<MainContent>
<Container maxWidth="md">
<Logo />
<Box textAlign="center" mb={3}>
<Container maxWidth="xs">
<Typography variant="h1" sx={{ mt: 4, mb: 2 }}>
Coming Soon
</Typography>
<Typography
variant="h3"
color="text.secondary"
fontWeight="normal"
sx={{ mb: 4 }}
>
We're working on implementing the last features before our
launch!
</Typography>
</Container>
<img
alt="Coming Soon"
height={200}
src="/static/images/status/coming-soon.svg"
/>
</Box>
<Box display="flex" justifyContent="center">
{timerComponents.length ? timerComponents : <>Time's up!</>}
</Box>
<Container maxWidth="sm">
<Box sx={{ textAlign: 'center', p: 4 }}>
<FormControl variant="outlined" fullWidth>
<OutlinedInputWrapper
type="text"
placeholder="Enter your email address here..."
endAdornment={
<InputAdornment position="end">
<ButtonNotify variant="contained" size="small">
Notify Me
</ButtonNotify>
</InputAdornment>
}
startAdornment={
<InputAdornment position="start">
<MailTwoToneIcon />
</InputAdornment>
}
/>
<FormHelperText>
We'll email you once our website is launched!
</FormHelperText>
</FormControl>
<Divider sx={{ my: 4 }} />
<Box sx={{ textAlign: 'center' }}>
<Tooltip arrow placement="top" title="Facebook">
<IconButton color="primary">
<FacebookIcon />
</IconButton>
</Tooltip>
<Tooltip arrow placement="top" title="Twitter">
<IconButton color="primary">
<TwitterIcon />
</IconButton>
</Tooltip>
<Tooltip arrow placement="top" title="Instagram">
<IconButton color="primary">
<InstagramIcon />
</IconButton>
</Tooltip>
</Box>
</Box>
</Container>
</Container>
</MainContent>
</>
);
}
export default StatusComingSoon;

View File

@ -0,0 +1,100 @@
import {
Box,
Typography,
Container,
Divider,
IconButton,
Tooltip
} from '@mui/material';
import { Helmet } from 'react-helmet-async';
import Logo from 'src/components/LogoSign';
import { styled } from '@mui/material/styles';
import FacebookIcon from '@mui/icons-material/Facebook';
import TwitterIcon from '@mui/icons-material/Twitter';
import InstagramIcon from '@mui/icons-material/Instagram';
const MainContent = styled(Box)(
() => `
height: 100%;
display: flex;
flex: 1;
overflow: auto;
flex-direction: column;
align-items: center;
justify-content: center;
`
);
function StatusMaintenance() {
return (
<>
<Helmet>
<title>Status - Maintenance</title>
</Helmet>
<MainContent>
<Container maxWidth="md">
<Logo />
<Box textAlign="center">
<Container maxWidth="xs">
<Typography variant="h2" sx={{ mt: 4, mb: 2 }}>
The site is currently down for maintenance
</Typography>
<Typography
variant="h3"
color="text.secondary"
fontWeight="normal"
sx={{ mb: 4 }}
>
We apologize for any inconveniences caused
</Typography>
</Container>
<img
alt="Maintenance"
height={250}
src="/static/images/status/maintenance.svg"
/>
</Box>
<Divider sx={{ my: 4 }} />
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
>
<Box>
<Typography component="span" variant="subtitle1">
Phone:{' '}
</Typography>
<Typography
component="span"
variant="subtitle1"
color="text.primary"
>
+ 00 1 888 555 444
</Typography>
</Box>
<Box>
<Tooltip arrow placement="top" title="Facebook">
<IconButton color="primary">
<FacebookIcon />
</IconButton>
</Tooltip>
<Tooltip arrow placement="top" title="Twitter">
<IconButton color="primary">
<TwitterIcon />
</IconButton>
</Tooltip>
<Tooltip arrow placement="top" title="Instagram">
<IconButton color="primary">
<InstagramIcon />
</IconButton>
</Tooltip>
</Box>
</Box>
</Container>
</MainContent>
</>
);
}
export default StatusMaintenance;

View File

@ -0,0 +1,95 @@
import {
Box,
Card,
Typography,
Container,
Divider,
Button,
FormControl,
OutlinedInput,
InputAdornment,
styled
} from '@mui/material';
import { Helmet } from 'react-helmet-async';
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
const MainContent = styled(Box)(
({ theme }) => `
height: 100%;
display: flex;
flex: 1;
overflow: auto;
flex-direction: column;
align-items: center;
justify-content: center;
`
);
const OutlinedInputWrapper = styled(OutlinedInput)(
({ theme }) => `
background-color: ${theme.colors.alpha.white[100]};
`
);
const ButtonSearch = styled(Button)(
({ theme }) => `
margin-right: -${theme.spacing(1)};
`
);
function Status404() {
return (
<>
<Helmet>
<title>Status - 404</title>
</Helmet>
<MainContent>
<Container maxWidth="md">
<Box textAlign="center">
<img alt="404" height={180} src="/static/images/status/404.svg" />
<Typography variant="h2" sx={{ my: 2 }}>
The page you were looking for doesn't exist.
</Typography>
<Typography
variant="h4"
color="text.secondary"
fontWeight="normal"
sx={{ mb: 4 }}
>
It's on us, we moved the content to a different page. The search
below should help!
</Typography>
</Box>
<Container maxWidth="sm">
<Card sx={{ textAlign: 'center', mt: 3, p: 4 }}>
<FormControl variant="outlined" fullWidth>
<OutlinedInputWrapper
type="text"
placeholder="Search terms here..."
endAdornment={
<InputAdornment position="end">
<ButtonSearch variant="contained" size="small">
Search
</ButtonSearch>
</InputAdornment>
}
startAdornment={
<InputAdornment position="start">
<SearchTwoToneIcon />
</InputAdornment>
}
/>
</FormControl>
<Divider sx={{ my: 4 }}>OR</Divider>
<Button href="/overview" variant="outlined">
Go to homepage
</Button>
</Card>
</Container>
</Container>
</MainContent>
</>
);
}
export default Status404;

View File

@ -0,0 +1,142 @@
import { useState } from 'react';
import {
Box,
Button,
Container,
Grid,
Hidden,
styled,
Typography
} from '@mui/material';
import { Helmet } from 'react-helmet-async';
import RefreshTwoToneIcon from '@mui/icons-material/RefreshTwoTone';
import LoadingButton from '@mui/lab/LoadingButton';
const GridWrapper = styled(Grid)(
({ theme }) => `
background: ${theme.colors.gradients.black1};
`
);
const MainContent = styled(Box)(
() => `
height: 100%;
display: flex;
flex: 1;
overflow: auto;
flex-direction: column;
align-items: center;
justify-content: center;
`
);
const TypographyPrimary = styled(Typography)(
({ theme }) => `
color: ${theme.colors.alpha.white[100]};
`
);
const TypographySecondary = styled(Typography)(
({ theme }) => `
color: ${theme.colors.alpha.white[70]};
`
);
function Status500() {
const [pending, setPending] = useState(false);
function handleClick() {
setPending(true);
}
return (
<>
<Helmet>
<title>Status - 500</title>
</Helmet>
<MainContent>
<Grid
container
sx={{ height: '100%' }}
alignItems="stretch"
spacing={0}
>
<Grid
xs={12}
md={6}
alignItems="center"
display="flex"
justifyContent="center"
item
>
<Container maxWidth="sm">
<Box textAlign="center">
<img
alt="500"
height={260}
src="/static/images/status/500.svg"
/>
<Typography variant="h2" sx={{ my: 2 }}>
There was an error, please try again later
</Typography>
<Typography
variant="h4"
color="text.secondary"
fontWeight="normal"
sx={{ mb: 4 }}
>
The server encountered an internal error and was not able to
complete your request
</Typography>
<LoadingButton
onClick={handleClick}
loading={pending}
variant="outlined"
color="primary"
startIcon={<RefreshTwoToneIcon />}
>
Refresh view
</LoadingButton>
<Button href="/overview" variant="contained" sx={{ ml: 1 }}>
Go back
</Button>
</Box>
</Container>
</Grid>
<Hidden mdDown>
<GridWrapper
xs={12}
md={6}
alignItems="center"
display="flex"
justifyContent="center"
item
>
<Container maxWidth="sm">
<Box textAlign="center">
<TypographyPrimary variant="h1" sx={{ my: 2 }}>
InnovEnergy{' '}
</TypographyPrimary>
<TypographySecondary
variant="h4"
fontWeight="normal"
sx={{ mb: 4 }}
>
High performance React template built with lots of powerful
Material-UI components across multiple product niches for
fast &amp; perfect apps development processes.
</TypographySecondary>
<Button href="/overview" size="large" variant="contained">
Overview
</Button>
</Box>
</Container>
</GridWrapper>
</Hidden>
</Grid>
</MainContent>
</>
);
}
export default Status500;

View File

@ -0,0 +1,168 @@
import {
createContext,
ReactNode,
useCallback,
useContext,
useState
} from 'react';
import axiosConfig from 'src/Resources/axiosConfig';
import { TokenContext } from './tokenContext';
import {
I_UserWithInheritedAccess,
InnovEnergyUser
} from '../interfaces/UserTypes';
interface AccessContextProviderProps {
usersWithDirectAccess: InnovEnergyUser[];
fetchUsersWithDirectAccessForResource: (
tempresourceType: string,
id: number
) => void;
usersWithInheritedAccess: I_UserWithInheritedAccess[];
fetchUsersWithInheritedAccessForResource: (
tempresourceType: string,
id: number
) => void;
error: boolean;
setError: (value: boolean) => void;
updated: boolean;
setUpdated: (value: boolean) => void;
updatedmessage: string;
errormessage: string;
setErrorMessage: (value: string) => void;
setUpdatedMessage: (value: string) => void;
RevokeAccessFromResource: (
resourceType: string,
id: number,
resourceId: string,
current_ResourceId: number,
name: string
) => void;
}
export const AccessContext = createContext<AccessContextProviderProps>({
usersWithDirectAccess: [],
fetchUsersWithDirectAccessForResource: () => Promise.resolve(),
usersWithInheritedAccess: [],
fetchUsersWithInheritedAccessForResource: () => Promise.resolve(),
error: false,
setError: () => {},
updated: false,
setUpdated: () => {},
updatedmessage: '',
errormessage: '',
setErrorMessage: () => {},
setUpdatedMessage: () => {},
RevokeAccessFromResource: () => Promise.resolve()
});
const AccessContextProvider = ({ children }: { children: ReactNode }) => {
const [error, setError] = useState(false);
const [errormessage, setErrorMessage] = useState('An error has occured');
const [updated, setUpdated] = useState(false);
const [updatedmessage, setUpdatedMessage] = useState('Successfully updated');
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const [usersWithDirectAccess, setUsersWithDirectAccess] = useState<
InnovEnergyUser[]
>([]);
const [usersWithInheritedAccess, setUsersWithInheritedAccess] = useState<
I_UserWithInheritedAccess[]
>([]);
const fetchUsersWithDirectAccessForResource = useCallback(
async (tempresourceType: string, id: number) => {
axiosConfig
.get(`/GetUsersWithDirectAccess${tempresourceType}?id=${id}`)
.then((response) => {
if (response) {
setUsersWithDirectAccess(response.data);
}
})
.catch((error) => {
setError(true);
setErrorMessage('Unable to load data');
});
},
[]
);
const fetchUsersWithInheritedAccessForResource = useCallback(
async (tempresourceType: string, id: number) => {
axiosConfig
.get(`/GetUsersWithInheritedAccess${tempresourceType}?id=${id}`)
.then((response) => {
if (response) {
setUsersWithInheritedAccess(response.data);
}
})
.catch((error) => {
setError(true);
setErrorMessage('Unable to load data');
});
},
[]
);
const RevokeAccessFromResource = useCallback(
async (
resourceType: string,
id: number,
resourceId: string,
current_ResourceId: number,
name: string
) => {
axiosConfig
.post(
`/RevokeUserAccess${resourceType}?UserId=${id}&${resourceId}=${current_ResourceId}`
)
.then((response) => {
if (response) {
fetchUsersWithDirectAccessForResource(
resourceType,
current_ResourceId
);
fetchUsersWithInheritedAccessForResource(
resourceType,
current_ResourceId
);
setUpdatedMessage('Revoked access from user: ' + name);
setUpdated(true);
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setError(true);
setErrorMessage('Unable to revoke access');
});
},
[]
);
return (
<AccessContext.Provider
value={{
usersWithDirectAccess,
fetchUsersWithDirectAccessForResource,
usersWithInheritedAccess,
fetchUsersWithInheritedAccessForResource,
error,
setError,
updated,
setUpdated,
updatedmessage,
errormessage,
setErrorMessage,
setUpdatedMessage,
RevokeAccessFromResource
}}
>
{children}
</AccessContext.Provider>
);
};
export default AccessContextProvider;

View File

@ -0,0 +1,260 @@
import { AxiosError } from 'axios';
import {
createContext,
ReactNode,
useCallback,
useContext,
useState
} from 'react';
import axiosConfig from 'src/Resources/axiosConfig';
import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
import { TokenContext } from './tokenContext';
interface I_InstallationContextProviderProps {
data: I_Installation[];
fetchAllInstallations: () => Promise<void>;
fetchAllFoldersAndInstallations: () => Promise<void>;
createInstallation: (value: Partial<I_Installation>) => Promise<void>;
updateInstallation: (value: I_Installation, view: string) => Promise<void>;
loading: boolean;
setLoading: (value: boolean) => void;
error: boolean;
setError: (value: boolean) => void;
updated: boolean;
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: [],
fetchAllInstallations: () => Promise.resolve(),
fetchAllFoldersAndInstallations: () => Promise.resolve(),
createInstallation: () => Promise.resolve(),
updateInstallation: () => Promise.resolve(),
loading: false,
setLoading: () => {},
error: false,
setError: () => {},
updated: false,
setUpdated: () => {},
deleteInstallation: () => Promise.resolve(),
createFolder: () => Promise.resolve(),
updateFolder: () => Promise.resolve(),
deleteFolder: () => Promise.resolve()
});
const InstallationsContextProvider = ({
children
}: {
children: ReactNode;
}) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [updated, setUpdated] = useState(false);
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const fetchAllInstallations = useCallback(async () => {
let isMounted = true;
axiosConfig
.get('/GetAllInstallations', {})
.then((res) => {
setData(res.data);
})
.catch((err: AxiosError) => {
if (err.response && err.response.status == 401) {
removeToken();
}
});
}, []);
const fetchAllFoldersAndInstallations = useCallback(async () => {
return axiosConfig
.get('/GetAllFoldersAndInstallations')
.then((res) => {
setData(res.data);
})
.catch((err) => {
if (err.response && err.response.status == 401) {
removeToken();
}
});
}, [setData]);
const createInstallation = useCallback(
async (formValues: Partial<I_Installation>) => {
axiosConfig
.post('/CreateInstallation', formValues)
.then((res) => {
setLoading(false);
fetchAllFoldersAndInstallations();
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
},
[]
);
const updateInstallation = useCallback(
async (formValues: I_Installation, view: string) => {
axiosConfig
.put('/UpdateInstallation', formValues)
.then((response) => {
if (response) {
setLoading(false);
setUpdated(true);
if (view == 'installation') {
fetchAllInstallations();
} else {
fetchAllFoldersAndInstallations();
}
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
},
[]
);
const deleteInstallation = useCallback(
async (formValues: I_Installation, view: string) => {
axiosConfig
.delete(`/DeleteInstallation?installationId=${formValues.id}`)
.then((response) => {
if (response) {
setLoading(false);
setUpdated(true);
if (view == 'installation') {
fetchAllInstallations();
} else {
fetchAllFoldersAndInstallations();
}
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
},
[]
);
const createFolder = useCallback(async (formValues: Partial<I_Folder>) => {
axiosConfig
.post('/CreateFolder', formValues)
.then((res) => {
setLoading(false);
fetchAllFoldersAndInstallations();
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
}, []);
const updateFolder = useCallback(async (formValues: I_Folder) => {
axiosConfig
.put('/UpdateFolder', formValues)
.then((response) => {
if (response) {
setLoading(false);
setUpdated(true);
fetchAllFoldersAndInstallations();
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
}, []);
const deleteFolder = useCallback(async (formValues: I_Folder) => {
axiosConfig
.delete(`/DeleteFolder?folderId=${formValues.id}`)
.then((response) => {
if (response) {
setLoading(false);
setUpdated(true);
fetchAllFoldersAndInstallations();
setTimeout(() => {
setUpdated(false);
}, 3000);
}
})
.catch((error) => {
setLoading(false);
setError(true);
if (error.response && error.response.status == 401) {
removeToken();
}
});
}, []);
return (
<InstallationsContext.Provider
value={{
data,
fetchAllInstallations,
fetchAllFoldersAndInstallations,
createInstallation,
updateInstallation,
loading,
setLoading,
error,
setError,
updated,
setUpdated,
deleteInstallation,
createFolder,
updateFolder,
deleteFolder
}}
>
{children}
</InstallationsContext.Provider>
);
};
export default InstallationsContextProvider;

View File

@ -0,0 +1,69 @@
import { createContext, ReactNode, useState } from 'react';
interface LogContextProviderProps {
installationStatus: Record<number, number[]>;
handleLogWarningOrError: (installation_id: number, value: number) => void;
getStatus: (installationId: number) => number;
}
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 handleLogWarningOrError = (installation_id: number, value: number) => {
setInstallationStatus((prevStatus) => {
const newStatus = { ...prevStatus };
if (!newStatus.hasOwnProperty(installation_id)) {
newStatus[installation_id] = [];
}
newStatus[installation_id].unshift(value);
newStatus[installation_id] = newStatus[installation_id].slice(0, 5);
return newStatus;
});
};
const getStatus = (installationId: number) => {
let status;
if (!installationStatus.hasOwnProperty(installationId)) {
status = -2;
} else {
if (installationStatus[installationId][0] == -1) {
let i = 0;
for (i; i < installationStatus[installationId].length; i++) {
if (installationStatus[installationId][i] != -1) {
break;
}
}
if (i === installationStatus[installationId].length) {
status = -1;
}
} else {
status = installationStatus[installationId][0];
}
}
return status;
};
return (
<LogContext.Provider
value={{
installationStatus,
handleLogWarningOrError,
getStatus
}}
>
{children}
</LogContext.Provider>
);
};
export default LogContextProvider;

View File

@ -0,0 +1,30 @@
import { createContext, FC, useState } from 'react';
type SidebarContext = {
sidebarToggle: any;
toggleSidebar: () => void;
closeSidebar: () => void;
};
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const SidebarContext = createContext<SidebarContext>(
{} as SidebarContext
);
export const SidebarProvider: FC = ({ children }) => {
const [sidebarToggle, setSidebarToggle] = useState(false);
const toggleSidebar = () => {
setSidebarToggle(!sidebarToggle);
};
const closeSidebar = () => {
setSidebarToggle(false);
};
return (
<SidebarContext.Provider
value={{ sidebarToggle, toggleSidebar, closeSidebar }}
>
{children}
</SidebarContext.Provider>
);
};

View File

@ -0,0 +1,114 @@
import { createContext, ReactNode, useCallback, useState } from 'react';
import { useParams } from 'react-router-dom';
import axiosConfig from 'src/Resources/axiosConfig';
import {
I_UserWithInheritedAccess,
InnovEnergyUser
} from 'src/interfaces/UserTypes';
interface I_UsersContextProviderProps {
directAccessUsers: InnovEnergyUser[];
setDirectAccessUsers: (value: InnovEnergyUser[]) => void;
inheritedAccessUsers: I_UserWithInheritedAccess[];
setInheritedAccessUsers: (value: I_UserWithInheritedAccess[]) => void;
availableUsers: InnovEnergyUser[];
setAvailableUsers: (value: InnovEnergyUser[]) => void;
getAvailableUsersForResource: () => InnovEnergyUser[];
fetchUsersWithInheritedAccessForResource: () => Promise<void>;
fetchUsersWithDirectAccessForResource: () => Promise<void>;
fetchAvailableUsers: () => Promise<void>;
}
export const UsersContext = createContext<I_UsersContextProviderProps>({
directAccessUsers: [],
setDirectAccessUsers: () => {},
inheritedAccessUsers: [],
setInheritedAccessUsers: () => {},
availableUsers: [],
setAvailableUsers: () => {},
getAvailableUsersForResource: () => {
return [];
},
fetchUsersWithInheritedAccessForResource: () => {
return Promise.resolve();
},
fetchUsersWithDirectAccessForResource: () => {
return Promise.resolve();
},
fetchAvailableUsers: () => {
return Promise.resolve();
}
});
const UsersContextProvider = ({ children }: { children: ReactNode }) => {
const [directAccessUsers, setDirectAccessUsers] = useState<InnovEnergyUser[]>(
[]
);
const [inheritedAccessUsers, setInheritedAccessUsers] = useState<
I_UserWithInheritedAccess[]
>([]);
const [availableUsers, setAvailableUsers] = useState<InnovEnergyUser[]>([]);
const currentType = 13;
const { id } = useParams();
const fetchUsersWithAccess = useCallback(
async (route: string, setState: (value: any) => void) => {
axiosConfig.get(route + currentType, { params: { id } }).then((res) => {
setState(res.data);
});
},
[currentType, id]
);
const fetchUsersWithInheritedAccessForResource = useCallback(async () => {
fetchUsersWithAccess(
'/GetUsersWithInheritedAccessTo',
setInheritedAccessUsers
);
}, [fetchUsersWithAccess]);
const fetchUsersWithDirectAccessForResource = useCallback(async () => {
fetchUsersWithAccess('/GetUsersWithDirectAccessTo', setDirectAccessUsers);
}, [fetchUsersWithAccess]);
const getAvailableUsersForResource = () => {
const inheritedUsers = inheritedAccessUsers.map(
(inheritedAccessUser) => inheritedAccessUser.user
);
const allUsersWithAccess = [...inheritedUsers, ...directAccessUsers];
return availableUsers.filter(
(availableUser) =>
!allUsersWithAccess.find(
(userWithAccess) => availableUser.id === userWithAccess.id
)
);
};
const fetchAvailableUsers = async (): Promise<void> => {
return axiosConfig.get('/GetAllChildUsers').then((res) => {
setAvailableUsers(res.data);
});
};
return (
<UsersContext.Provider
value={{
directAccessUsers,
setDirectAccessUsers,
inheritedAccessUsers,
setInheritedAccessUsers,
availableUsers,
setAvailableUsers,
getAvailableUsersForResource,
fetchUsersWithInheritedAccessForResource,
fetchUsersWithDirectAccessForResource,
fetchAvailableUsers
}}
>
{children}
</UsersContext.Provider>
);
};
export default UsersContextProvider;

View File

@ -0,0 +1,47 @@
import {createContext, ReactNode, useState} from 'react';
//const setUser=(currentUser: InnovEnergyUser)=>{
// localStorage.setItem("currentUser",JSON.stringify(currentUser));
//}
// Define the shape of the context
interface TokenContextType {
token?: string | null;
setNewToken: (new_token: string) => void;
removeToken: () => void;
}
// Create the context.
export const TokenContext = createContext<TokenContextType | undefined>(
undefined
);
// Create a UserContextProvider component
export const TokenContextProvider = ({ children }: { children: ReactNode }) => {
//Initialize context state with a "null" user
const [token, setToken] = useState(localStorage.getItem('token'));
const saveToken = (new_token: string) => {
setToken(new_token);
localStorage.setItem('token', new_token);
};
const deleteToken = () => {
localStorage.removeItem('token');
setToken(null);
};
return (
<TokenContext.Provider
value={{
token,
setNewToken: saveToken,
removeToken: deleteToken
}}
>
{children}
</TokenContext.Provider>
);
};
export default TokenContextProvider;

View File

@ -0,0 +1,45 @@
import {createContext, ReactNode, useState} from 'react';
import {InnovEnergyUser} from '../interfaces/UserTypes';
// Define the shape of the context
interface UserContextType {
currentUser?: InnovEnergyUser;
setUser: (user: InnovEnergyUser) => void;
removeUser: () => void;
}
// Create the context.
export const UserContext = createContext<UserContextType | undefined>(
undefined
);
// Create a UserContextProvider component
export const UserContextProvider = ({ children }: { children: ReactNode }) => {
//Initialize context state with a "null" user
const [currentUser, setUser] = useState<InnovEnergyUser>(
JSON.parse(localStorage.getItem('currentUser'))
);
const saveUser = (new_user: InnovEnergyUser) => {
setUser(new_user);
localStorage.setItem('currentUser', JSON.stringify(new_user));
};
const deleteUser = () => {
localStorage.removeItem('currentUser');
};
return (
<UserContext.Provider
value={{
currentUser,
setUser: saveUser,
removeUser: deleteUser
}}
>
{children}
</UserContext.Provider>
);
};
export default UserContextProvider;

View File

@ -0,0 +1,78 @@
import { sha1Hmac } from "./Sha1";
import { Utf8 } from "./Utf8";
import { toBase64 } from "./UInt8Utils";
export class S3Access {
constructor(
readonly bucket: string,
readonly region: string,
readonly provider: string,
readonly key: string,
readonly secret: string
) {}
get host(): string {
return `${this.region}.${this.provider}`;
}
get url(): string {
return `https://${this.host}`;
}
public get(s3Path: string): Promise<Response> {
const method = "GET";
const auth = this.createAuthorizationHeader(method, s3Path, "");
const url = this.url + "/" + this.bucket + "/" + s3Path;
const headers = { Host: this.host, Authorization: auth };
try {
return fetch(url, { method: method, mode: "cors", headers: headers });
} catch {
return Promise.reject();
}
}
private createAuthorizationHeader(
method: string,
s3Path: string,
date: string
) {
return createAuthorizationHeader(
method,
this.bucket,
s3Path,
date,
this.key,
this.secret
);
}
}
function createAuthorizationHeader(
method: string,
bucket: string,
s3Path: string,
date: string,
s3Key: string,
s3Secret: string,
contentType: string = "",
md5Hash: string = ""
) {
// StringToSign = HTTP-Verb + "\n" +
// Content-MD5 + "\n" +
// Content-Type + "\n" +
// Date + "\n" +
// CanonicalizedAmzHeaders +
// CanonicalizedResource;
const payload = Utf8.encode(
`${method}\n${md5Hash}\n${contentType}\n${date}\n/${bucket}/${s3Path}`
);
//console.log(`${method}\n${md5Hash}\n${contentType}\n${date}\n/${bucket}/${s3Path}`)
const secret = Utf8.encode(s3Secret);
const signature = toBase64(sha1Hmac(payload, secret));
return `AWS ${s3Key}:${signature}`;
}

View File

@ -0,0 +1,125 @@
import {concat, pad} from "./UInt8Utils";
const BigEndian = false
export function sha1Hmac(msg: Uint8Array, key: Uint8Array): Uint8Array
{
if (key.byteLength > 64)
key = sha1(key)
if (key.byteLength < 64)
key = pad(key, 64)
const oKey = key.map(b => b ^ 0x5C);
const iKey = key.map(b => b ^ 0x36);
const iData = concat(iKey, msg);
const iHash = sha1(iData);
const oData = concat(oKey, iHash);
return sha1(oData);
}
export function sha1(data: Uint8Array): Uint8Array
{
const paddedData: DataView = initData(data)
const H = new Uint32Array([0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0])
const S = new Uint32Array(5) // State
const round = new Uint32Array(80);
function initRound(startOffset: number)
{
for (let i = 0; i < 16; i++)
round[i] = paddedData.getUint32((startOffset + i) * 4, BigEndian);
for (let i = 16; i < 80; i++)
{
const int32 = round[i - 3] ^ round[i - 8] ^ round[i - 14] ^ round[i - 16];
round[i] = rotate1(int32); // SHA0 has no rotate
}
}
const functions =
[
() => (S[1] & S[2] | ~S[1] & S[3]) + 0x5A827999,
() => (S[1] ^ S[2] ^ S[3]) + 0x6ED9EBA1,
() => (S[1] & S[2] | S[1] & S[3] | S[2] & S[3]) + 0x8F1BBCDC,
() => (S[1] ^ S[2] ^ S[3]) + 0xCA62C1D6
]
for (let startOffset = 0; startOffset < paddedData.byteLength / 4; startOffset += 16)
{
initRound(startOffset);
S.set(H)
for (let r = 0, i = 0; r < 4; r++)
{
const f = functions[r]
const end = i + 20;
do
{
const S0 = rotate5(S[0]) + f() + S[4] + round[i];
S[4] = S[3];
S[3] = S[2];
S[2] = rotate30(S[1]);
S[1] = S[0];
S[0] = S0;
}
while (++i < end)
}
for (let i = 0; i < 5; i++)
H[i] += S[i]
}
swapEndianness(H);
return new Uint8Array(H.buffer)
}
function rotate5(int32: number)
{
return (int32 << 5) | (int32 >>> 27); // >>> for unsigned shift
}
function rotate30(int32: number)
{
return (int32 << 30) | (int32 >>> 2);
}
function rotate1(int32: number)
{
return (int32 << 1) | (int32 >>> 31);
}
function initData(data: Uint8Array): DataView
{
const dataLength = data.length
const extendedLength = dataLength + 9; // add 8 bytes for UInt64 length + 1 byte for "stop-bit" (0x80)
const paddedLength = Math.ceil(extendedLength / 64) * 64; // pad to 512 bits block
const paddedData = new Uint8Array(paddedLength)
paddedData.set(data)
paddedData[dataLength] = 0x80 // append single 1 bit at end of data
const dataView = new DataView(paddedData.buffer)
// append UInt64 length
dataView.setUint32(paddedData.length - 4, dataLength << 3 , BigEndian) // dataLength in *bits* LO, (<< 3: x8 bits per byte)
dataView.setUint32(paddedData.length - 8, dataLength >>> 29, BigEndian) // dataLength in *bits* HI
return dataView
}
function swapEndianness(uint32Array: Uint32Array)
{
const dv = new DataView(uint32Array.buffer)
for (let i = 0; i < uint32Array.byteLength; i += 4)
{
const uint32 = dv.getUint32(i, false)
dv.setUint32(i, uint32, true)
}
}

View File

@ -0,0 +1,56 @@
export function pad(data: Uint8Array, length: number): Uint8Array
{
if (length < data.byteLength)
throw new RangeError("length")
const padded = new Uint8Array(length)
padded.set(data)
return padded;
}
export function concat(left: Uint8Array, right: Uint8Array): Uint8Array
{
const c = new Uint8Array(left.length + right.length);
c.set(left);
c.set(right, left.length);
return c
}
export function toHexString(data: Uint8Array)
{
return [...data].map(byteToHex).join('');
}
function byteToHex(b: number)
{
return b.toString(16).padStart(2, "0");
}
const b64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
export function toBase64(data : Uint8Array) : string
{
const byteLength = data.byteLength
const base64LengthPadded = 4 * Math.ceil(byteLength / 3)
const base64Length = Math.ceil(byteLength / 3 * 4);
const base64 = new Array<String>(base64LengthPadded)
for (let i = 0, o = 0; i < byteLength;)
{
const x = data[i++]
const y = data[i++] ?? 0
const z = data[i++] ?? 0
base64[o++] = b64Chars[x >>> 2]
base64[o++] = b64Chars[(x << 4 | y >>> 4) & 63]
base64[o++] = b64Chars[(y << 2 | z >>> 6) & 63]
base64[o++] = b64Chars[z & 63]
}
for (let i = base64LengthPadded; i > base64Length ;)
base64[--i] = "="
return base64.join('')
}

View File

@ -0,0 +1,10 @@
export namespace Utf8
{
const encoder = new TextEncoder()
const decoder = new TextDecoder()
export const encode = (text: string): Uint8Array => encoder.encode(text);
export const decode = (data: Uint8Array): string => decoder.decode(data);
}

View File

@ -0,0 +1,30 @@
import { Maybe } from 'yup';
import { Timestamped } from './types';
import { isDefined } from './utils/maybe';
import { I_CsvEntry } from 'src/content/dashboards/Log/graph.util';
export type DataRecord = Record<string, I_CsvEntry>;
export type DataPoint = Timestamped<Maybe<DataRecord>>;
export type RecordSeries = Array<DataPoint>;
export type PointSeries = Array<Timestamped<Maybe<I_CsvEntry>>>;
export type DataSeries = Array<Maybe<I_CsvEntry>>;
export function getPoints(
recordSeries: RecordSeries,
series: keyof DataRecord
): PointSeries {
return recordSeries.map((p) => ({
time: p.time,
value: isDefined(p.value) ? p.value[series] : undefined
}));
}
export function getData(
recordSeries: RecordSeries,
series: keyof DataRecord
): DataSeries {
return recordSeries.map((p) =>
isDefined(p.value) ? p.value[series] : undefined
);
}

View File

@ -0,0 +1,174 @@
/* eslint-disable no-mixed-operators */
import {TimeSpan, UnixTime} from './time';
import {Observable, Subject} from 'rxjs';
import {SkipList} from './skipList/skipList';
import {createDispatchQueue} from './promiseQueue';
import {SkipListNode} from './skipList/skipListNode';
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';
export const FetchResult = {
notAvailable: 'N/A',
tryLater: 'Try Later'
} as const;
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type FetchResult<T> =
| T
| typeof FetchResult.notAvailable
| typeof FetchResult.tryLater;
function reverseBits(x: number): number {
// https://stackoverflow.com/a/60227327/141397
x = ((x & 0x55555555) << 1) | ((x & 0xaaaaaaaa) >> 1);
x = ((x & 0x33333333) << 2) | ((x & 0xcccccccc) >> 2);
x = ((x & 0x0f0f0f0f) << 4) | ((x & 0xf0f0f0f0) >> 4);
x = ((x & 0x00ff00ff) << 8) | ((x & 0xff00ff00) >> 8);
x = ((x & 0x0000ffff) << 16) | ((x & 0xffff0000) >> 16);
return x >>> 0;
}
export default class DataCache<T extends Record<string, I_CsvEntry>> {
readonly _fetch: (t: UnixTime) => Promise<FetchResult<T>>;
public readonly gotData: Observable<UnixTime>;
private readonly cache: SkipList<Maybe<T>> = new SkipList<Maybe<T>>();
private readonly resolution: TimeSpan;
private readonly fetchQueue = createDispatchQueue(6);
private readonly fetching: Set<number> = new Set<number>();
constructor(
fetch: (t: UnixTime) => Promise<FetchResult<T>>,
resolution: TimeSpan
) {
this._fetch = fetch;
this.resolution = resolution;
this.gotData = new Subject<UnixTime>();
}
public prefetch(times: Array<UnixTime>, clear = true) {
if (clear) {
this.fetching.clear();
this.fetchQueue.clear();
}
const timesWithPriority = times.map((time, index) => ({
time,
priority: reverseBits(index)
}));
timesWithPriority.sort((x, y) => x.priority - y.priority);
for (let i = 0; i < timesWithPriority.length; i++) {
const time = timesWithPriority[i].time.round(this.resolution);
const t = time.ticks;
const node = this.cache.find(t);
if (node.index !== t) this.fetchData(time);
}
}
public get(timeStamp: UnixTime, fetch = true): Maybe<T> {
const time = timeStamp.round(this.resolution);
const t = time.ticks;
const node = this.cache.find(t);
if (node.index === t) return node.value;
if (fetch) this.fetchData(time);
return this.interpolate(node, t);
}
public getSeries(sampleTimes: UnixTime[]): RecordSeries {
this.prefetch(sampleTimes);
return sampleTimes.map((time) => ({ time, value: this.get(time, false) }));
}
private interpolate(before: SkipListNode<Maybe<T>>, t: number): Maybe<T> {
const dataBefore = before.value;
const after = before.next[0];
const dataAfter = after.value;
if (isUndefined(dataBefore) && isUndefined(dataAfter)) return undefined;
if (isUndefined(dataBefore)) return dataAfter;
if (isUndefined(dataAfter)) return dataBefore;
const p = t - before.index;
const n = after.index - t;
const pn = p + n;
let interpolated: Record<string, I_CsvEntry> = {};
//What about string nodes? like Alarms
for (const k of Object.keys(dataBefore)) {
const valueBefore = dataBefore[k].value;
const valueAfter = dataAfter[k]?.value as Maybe<number | string>;
let value: number | string;
if (isUndefined(valueAfter)) {
value = valueBefore;
} else if (isNumber(valueBefore) && isNumber(valueAfter)) {
value = (valueBefore * n + valueAfter * p) / pn;
} else {
value = n < p ? valueAfter : valueBefore;
}
interpolated[k] = { value, unit: dataBefore[k].unit };
}
return interpolated as T;
}
private fetchData(time: UnixTime) {
const t = time.ticks;
if (this.fetching.has(t))
// we are already fetching t
return;
const fetchTask = () => {
const onSuccess = (data: FetchResult<T>) => {
if (data === FetchResult.tryLater) {
console.warn(FetchResult.tryLater);
}
const value =
data === FetchResult.notAvailable || data === FetchResult.tryLater
? undefined
: data;
// @ts-ignore
this.cache.insert(value, t);
};
const onFailure = (_: unknown) => {
console.error(time.ticks + ' FAILED!'); // should not happen
};
const dispatch = () => {
this.fetching.delete(time.ticks);
(this.gotData as Subject<UnixTime>).next(time);
};
return this._fetch(time)
.then(
(d) => onSuccess(d),
(f) => onFailure(f)
)
.finally(() => dispatch());
};
this.fetching.add(t);
this.fetchQueue.dispatch(() => fetchTask());
}
}

View File

@ -0,0 +1,20 @@
// 0. Import Module
import { initializeLinq, IEnumerable } from "linq-to-typescript"
// 1. Declare that the JS types implement the IEnumerable interface
declare global {
interface Array<T> extends IEnumerable<T> { }
interface Uint8Array extends IEnumerable<number> { }
interface Uint8ClampedArray extends IEnumerable<number> { }
interface Uint16Array extends IEnumerable<number> { }
interface Uint32Array extends IEnumerable<number> { }
interface Int8Array extends IEnumerable<number> { }
interface Int16Array extends IEnumerable<number> { }
interface Int32Array extends IEnumerable<number> { }
interface Float32Array extends IEnumerable<number> { }
interface Float64Array extends IEnumerable<number> { }
interface Map<K, V> extends IEnumerable<[K, V]> { }
interface Set<T> extends IEnumerable<T> { }
interface String extends IEnumerable<string> { }
}
// 2. Bind Linq Functions to Array, Map, etc
initializeLinq()

View File

@ -0,0 +1,35 @@
import {map, MonoTypeOperatorFunction, Observable, tap} from "rxjs";
import {fastHash} from "./utils";
type ConcatX<T extends readonly (readonly any[])[]> = [
...T[0], ...T[1], ...T[2], ...T[3], ...T[4],
...T[5], ...T[6], ...T[7], ...T[8], ...T[9],
...T[10], ...T[11], ...T[12], ...T[13], ...T[14],
...T[15], ...T[16], ...T[17], ...T[18], ...T[19]
];
type Flatten<T extends readonly any[]> =
ConcatX<[...{ [K in keyof T]: T[K] extends any[] ? T[K] : [T[K]] }, ...[][]]>
export function flatten()
{
return function<T extends Array<unknown>>(source: Observable<T>)
{
return source.pipe
(
map(a => a.flat() as Flatten<T>)
)
}
}
type RecursiveObject<T> = T extends object ? T : never;
type Terminals<TModel, T> =
{
[Key in keyof TModel]: TModel[Key] extends RecursiveObject<TModel[Key]>
? Terminals<TModel[Key], T>
: T;
};

View File

@ -0,0 +1,51 @@
export function createDispatchQueue(maxInflight: number, debug = false): { dispatch: (task: () => Promise<void>) => number; clear: () => void }
{
const queue: Array<() => Promise<void>> = []
let inflight = 0;
function done()
{
inflight--
if (debug && inflight + queue.length === 0)
console.log("queue empty")
if (inflight < maxInflight && queue.length > 0)
{
const task = queue.pop()!
inflight++
task().finally(() => done())
}
}
function dispatch(task: () => Promise<void>) : number
{
if (inflight < maxInflight)
{
inflight++;
task().finally(() => done())
}
else
{
if (debug && queue.length === 0)
console.log("queue in use")
queue.push(task)
}
return queue.length
}
function clear()
{
// https://stackoverflow.com/questions/1232040/how-do-i-empty-an-array-in-javascript
queue.length = 0
if (debug)
console.log("queue cleared")
}
return {dispatch, clear}
}

View File

@ -0,0 +1,79 @@
import { find, findPath, insert, Path, SkipListNode } from './skipListNode';
export class SkipList<T> {
public readonly head: SkipListNode<T>;
public readonly tail: SkipListNode<T>;
private readonly nLevels: number;
constructor(nLevels: number = 20) {
// TODO: auto-levels
this.tail = {
index: Number.MAX_VALUE,
next: [],
value: undefined!
};
this.head = {
index: Number.MIN_VALUE,
next: Array(nLevels).fill(this.tail),
value: undefined!
};
this.nLevels = nLevels;
}
private _length = 0;
get length(): number {
return this._length;
}
public find(
index: number,
startNode = this.head,
endNode = this.tail
): SkipListNode<T> {
return find(index, startNode, endNode);
}
public insert(value: T, index: number): SkipListNode<T> {
const path = this.findPath(index);
const node = path[0];
if (node.index === index) {
// overwrite
node.value = value;
return node;
}
const nodeToInsert = { value, index, next: [] } as SkipListNode<T>;
const rnd = (Math.random() * (1 << this.nLevels)) << 0;
for (let level = 0; level < this.nLevels; level++) {
insert(nodeToInsert, path[level], level);
if ((rnd & (1 << level)) === 0) break;
}
this._length += 1;
return nodeToInsert;
}
private findPath(
index: number,
startNode = this.head,
endNode = this.tail
): Path<T> {
return findPath(index, startNode, endNode);
}
// public remove(index: number): void
// {
// // TODO
// }
}

View File

@ -0,0 +1,52 @@
import {asMutableArray} from "../types";
export type Next<T> = { readonly next: ReadonlyArray<SkipListNode<T>> }
export type Index = { readonly index: number }
export type Indexed<T> = Index & { value: T }
export type SkipListNode<T> = Next<T> & Indexed<T>
export type Path<T> = SkipListNode<T>[];
export function find<T>(index: number, startNode: SkipListNode<T>, endNode: SkipListNode<T>): SkipListNode<T>
{
let node = startNode
for (let level = startNode.next.length - 1; level >= 0; level--)
node = findOnLevel(index, node, endNode, level)
return node
}
export function findOnLevel<T>(index: number, startNode: SkipListNode<T>, endNode: SkipListNode<T>, level: number): SkipListNode<T>
{
let node: SkipListNode<T> = startNode
while (true)
{
const next = node.next[level]
if (index < next.index || endNode.index < next.index)
return node
node = next
}
}
export function findPath<T>(index: number, startNode: SkipListNode<T>, endNode: SkipListNode<T>): Path<T>
{
const path = Array(startNode.next.length - 1)
let node = startNode
for (let level = startNode.next.length - 1; level >= 0; level--)
{
node = findOnLevel(index, node, endNode, level)
path[level] = node
}
return path
}
export function insert<T>(nodeToInsert: SkipListNode<T>, after: SkipListNode<T>, onLevel: number): void
{
asMutableArray(nodeToInsert.next)[onLevel] = after.next[onLevel]
asMutableArray(after.next)[onLevel] = nodeToInsert
}

View File

@ -0,0 +1,5 @@
export function trim(str: string, string: string = " "): string
{
const pattern = '^[' + string + ']*(.*?)[' + string + ']*$';
return str.replace(new RegExp(pattern), '$1')
}

View File

@ -0,0 +1,299 @@
import { trim } from './stringUtils';
export class UnixTime {
public static readonly Epoch = new UnixTime(0);
private constructor(readonly ticks: number) {}
public static now(): UnixTime {
return UnixTime.fromTicks(Date.now() / 1000);
}
public static fromDate(date: Date): UnixTime {
return UnixTime.fromTicks(date.getTime() / 1000);
}
public static fromTicks(ticks: number): UnixTime {
if (Math.floor(ticks) % 2 != 0) {
return new UnixTime(Math.floor(ticks) + 1);
}
return new UnixTime(Math.floor(ticks));
}
public toDate(): Date {
return new Date(this.ticks * 1000);
}
public later(timeSpan: TimeSpan): UnixTime {
return new UnixTime(this.ticks + timeSpan.ticks);
}
public move(ticks: number): UnixTime {
return new UnixTime(this.ticks + ticks);
}
public earlier(timeSpan: TimeSpan): UnixTime {
return new UnixTime(this.ticks - timeSpan.ticks);
}
public isEarlierThan(time: UnixTime): boolean {
return this.ticks < time.ticks;
}
public isEarlierThanOrEqual(time: UnixTime): boolean {
return this.ticks <= time.ticks;
}
public isLaterThan(time: UnixTime): boolean {
return this.ticks > time.ticks;
}
public isLaterThanOrEqual(time: UnixTime): boolean {
return this.ticks >= time.ticks;
}
public isEqual(time: UnixTime): boolean {
return this.ticks === time.ticks;
}
public isInTheFuture(): boolean {
return this.isLaterThan(UnixTime.now());
}
public isInThePast(): boolean {
return this.ticks < UnixTime.now().ticks;
}
public round(ticks: number): UnixTime;
public round(duration: TimeSpan): UnixTime;
public round(durationOrTicks: TimeSpan | number): UnixTime {
const ticks =
typeof durationOrTicks === 'number'
? durationOrTicks
: durationOrTicks.ticks;
return new UnixTime(Math.round(this.ticks / ticks) * ticks);
}
public rangeTo(time: UnixTime): TimeRange {
return TimeRange.fromTimes(this, time);
}
public rangeBefore(timeSpan: TimeSpan): TimeRange {
return TimeRange.fromTimes(this.earlier(timeSpan), this);
}
public rangeAfter(timeSpan: TimeSpan): TimeRange {
return TimeRange.fromTimes(this, this.later(timeSpan));
}
public toString(): string {
return this.ticks.toString();
}
}
export class TimeSpan {
private constructor(readonly ticks: number) {}
get milliSeconds(): number {
return this.ticks * 1000;
}
get seconds(): number {
return this.ticks;
}
get minutes(): number {
return this.ticks / 60;
}
get hours(): number {
return this.minutes / 60;
}
get days(): number {
return this.hours / 24;
}
get weeks(): number {
return this.days / 7;
}
public static fromTicks(t: number): TimeSpan {
return new TimeSpan(t);
}
public static fromSeconds(t: number): TimeSpan {
return TimeSpan.fromTicks(t);
}
public static fromMinutes(t: number): TimeSpan {
return TimeSpan.fromSeconds(t * 60);
}
public static fromHours(t: number): TimeSpan {
return TimeSpan.fromMinutes(t * 60);
}
public static fromDays(t: number): TimeSpan {
return TimeSpan.fromHours(t * 24);
}
public static fromWeeks(t: number): TimeSpan {
return TimeSpan.fromDays(t * 7);
}
public static span(from: UnixTime, to: UnixTime): TimeSpan {
return TimeSpan.fromTicks(Math.abs(to.ticks - from.ticks));
}
public add(timeSpan: TimeSpan): TimeSpan {
return TimeSpan.fromTicks(this.ticks + timeSpan.ticks);
}
public subtract(timeSpan: TimeSpan): TimeSpan {
return TimeSpan.fromTicks(this.ticks - timeSpan.ticks);
}
public divide(n: number): TimeSpan {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
if (n <= 0) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw 'n must be positive';
}
return TimeSpan.fromTicks(this.ticks / n);
}
public multiply(n: number): TimeSpan {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
if (n < 0) throw 'n cannot be negative';
return TimeSpan.fromTicks(this.ticks * n);
}
public round(ticks: number): TimeSpan;
public round(duration: TimeSpan): TimeSpan;
public round(durationOrTicks: TimeSpan | number): TimeSpan {
const ticks =
typeof durationOrTicks === 'number'
? durationOrTicks
: durationOrTicks.ticks;
return TimeSpan.fromTicks(Math.round(this.ticks / ticks) * ticks);
}
public toString(): string {
let dt = 60 * 60 * 24 * 7;
let ticks = this.ticks;
if (ticks === 0) return '0s';
ticks = Math.abs(ticks);
const nWeeks = Math.floor(ticks / dt);
ticks -= nWeeks * dt;
dt /= 7;
const nDays = Math.floor(ticks / dt);
ticks -= nDays * dt;
dt /= 24;
const nHours = Math.floor(ticks / dt);
ticks -= nHours * dt;
dt /= 60;
const nMinutes = Math.floor(ticks / dt);
ticks -= nMinutes * dt;
dt /= 60;
const nSeconds = Math.floor(ticks / dt);
let s = '';
if (nWeeks > 0) s += nWeeks.toString() + 'w ';
if (nDays > 0) s += nDays.toString() + 'd ';
if (nHours > 0) s += nHours.toString() + 'h ';
if (nMinutes > 0) s += nMinutes.toString() + 'm ';
if (nSeconds > 0) s += nSeconds.toString() + 's';
return trim(s);
}
}
export class TimeRange {
private constructor(
private readonly from: number,
private readonly to: number
) {}
public get start(): UnixTime {
return UnixTime.fromTicks(this.from);
}
public get mid(): UnixTime {
return UnixTime.fromTicks((this.from + this.to) / 2);
}
public get end(): UnixTime {
return UnixTime.fromTicks(this.to);
}
public get duration(): TimeSpan {
return TimeSpan.fromTicks(this.to - this.from);
}
public static fromTimes(from: UnixTime, to: UnixTime): TimeRange {
return from.isLaterThan(to)
? new TimeRange(to.ticks, from.ticks)
: new TimeRange(from.ticks, to.ticks);
}
public isInside(time: number): boolean;
public isInside(time: UnixTime): boolean;
public isInside(time: UnixTime | number) {
const t = time instanceof UnixTime ? time.ticks : time;
return t >= this.from && t < this.to;
}
public sample(period: TimeSpan): UnixTime[] {
const samples = [];
for (let t = this.from; t < this.to; t += period.ticks)
samples.push(UnixTime.fromTicks(t));
return samples;
}
public subdivide(n: number): TimeRange[] {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
if (n <= 0) throw 'n must be positive';
const period = TimeSpan.fromTicks(this.duration.ticks / n);
if (period === this.duration) return [this];
const samples = this.sample(period);
const ranges: TimeRange[] = [];
for (let i = 0; i < samples.length; )
ranges.push(TimeRange.fromTimes(samples[i], samples[++i]));
return ranges;
}
public earlier(dt: TimeSpan): TimeRange {
return new TimeRange(this.from - dt.ticks, this.to - dt.ticks);
}
public later(dt: TimeSpan): TimeRange {
return new TimeRange(this.from + dt.ticks, this.to + dt.ticks);
}
public move(ticks: number): TimeRange {
return new TimeRange(this.from + ticks, this.to + ticks);
}
}

View File

@ -0,0 +1,21 @@
import {UnixTime} from "./time";
export type Timestamped<T> = { time: UnixTime, value: T }
export type Pair<T1, T2 = T1> = [T1, T2]
export type Position = { readonly x: number, readonly y: number }
export type Direction = { readonly dx: number, readonly dy: number }
export type Size = { readonly width: number, readonly height: number }
export type Rect = Position & Size
export type Mutable<T> = { -readonly [P in keyof T]: T[P] };
export type FieldKey<T> = { [P in keyof T]: T[P] extends (...args: any) => any ? never : P }[keyof T];
export type AllFields<T> = Pick<T, FieldKey<T>>;
export type SomeFields<T> = Partial<AllFields<T>>
export const asMutable = <T>(t: T) => (t as Mutable<T>);
export const asMutableArray = <T>(t: ReadonlyArray<T>) => (t as Array<T>);
export const cast = <T>(t: unknown) => (t as T);
export type Rename<T, K extends keyof T, N extends string> = Pick<T, Exclude<keyof T, K>> & { [P in N]: T[K] }

View File

@ -0,0 +1,119 @@
import {IEnumerable} from "linq-to-typescript";
import { isDefined } from "./utils/maybe";
//export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
export type Nothing = Record<string, never>
// eslint-disable-next-line @typescript-eslint/no-empty-function
export function fastHash(str: string): number
{
const signed = str
.split('')
.reduce((p, c) => ((p << 5) - p) + c.charCodeAt(0) | 0, 0);
return Math.abs(signed);
}
// export function flattenObject(obj: object) : object
// {
// const flattened = {}
//
// for (const key of Object.keys(obj))
// {
// // @ts-ignore
// const value = obj[key]
//
// if (typeof value === 'object' && value !== null && !Array.isArray(value))
// {
// Object.assign(flattened, flattenObject(value))
// }
// else
// {
// // @ts-ignore
// flattened[key] = value
// }
// }
//
// return flattened
// }
//return function<TTerminal,TTree extends Terminals<TTree, TTerminal>>(source: Observable<TTerminal>)
export function* pairwise<T>(iterable: Iterable<T>, init?: T): Generator<[T, T]>
{
const it = iterable[Symbol.iterator]()
let first : T;
if (isDefined(init))
{
first = init
}
else
{
const f = it.next()
if (f.done)
return
first = f.value
}
let second = it.next()
while(!second.done)
{
yield [first, second.value]
first = second.value
second = it.next()
}
}
export function arraysEqual<T>(a: Array<T>, b: Array<T>)
{
if (a === b) return true;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; ++i)
{
if (a[i] !== b[i]) return false;
}
return true;
}
export function mod(a:number, b:number)
{
return ((a % b) + b) % b;
}
export function clamp(a: number, min: number, max: number)
{
return a > max ? max
: a < min ? min
: a
}
export function isDST(d : Date)
{
const jan = new Date(d.getFullYear(), 0, 1).getTimezoneOffset();
const jul = new Date(d.getFullYear(), 6, 1).getTimezoneOffset();
return Math.max(jan, jul) != d.getTimezoneOffset();
}
export function Transpose<T>(src: IEnumerable<IEnumerable<T>>): IEnumerable<IEnumerable<T>>
{
return src
.selectMany(line => line.select((element, column) => ({element, column})))
.groupBy(i => i.column)
.select(g => g.select(e => e.element));
}

View File

@ -0,0 +1,46 @@
import fs from "fs";
export function doesFileExist(path: string): boolean
{
try
{
fs.accessSync(path, fs.constants.F_OK);
return true;
}
catch (e)
{
return false;
}
}
export function doesDirExist(path: string): boolean
{
try
{
fs.readdirSync(path)
return true;
}
catch (e)
{
return false;
}
}
export function readJsonFile<T>(path: string)
{
const data = fs.readFileSync(path, "utf-8")
return JSON.parse(data) as T
}
export function writeJsonFile<T>(path: string, contents: T)
{
const data = JSON.stringify(contents)
return fs.writeFileSync(path, data, "utf-8")
}
export function writeJsonFilePretty<T>(path: string, contents: T)
{
const data = JSON.stringify(contents,undefined,2)
return fs.writeFileSync(path, data, "utf-8")
}

View File

@ -0,0 +1,127 @@
import MimeType from "./mime";
import fs from "fs";
import http, {IncomingMessage, ServerResponse} from "http";
import { Maybe } from "yup";
import { getLogger } from "./logging";
import { isDefined, isUndefined } from "./maybe";
import { Dictionary } from "./utilityTypes";
import { promisify } from "util";
import { entries } from "./utils";
const log = getLogger("HTTP")
const readFile = promisify(fs.readFile)
export type HttpResponse = {
body: Maybe<Buffer | string>
headers : Dictionary<string>
statusCode: number
}
function contentTypeHeader(mimeType: string)
{
return {'Content-type': mimeType};
}
function forbidden(message = "403 : forbidden", headers: Dictionary<string> = {}): HttpResponse
{
return text(message, 403, headers)
}
function notFound(message ="404 : not found", headers: Dictionary<string> = {}): HttpResponse
{
return text(message, 404, headers)
}
function text(text: string, statusCode = 200, headers: Dictionary<string> = {}): HttpResponse
{
return {
statusCode: statusCode,
headers : {...headers, ...contentTypeHeader('text/plain')},
body : text
};
}
function json(json: Dictionary,
replacer?: (k: string, v: unknown) => unknown,
headers: Dictionary<string> = {}): HttpResponse
{
return {
statusCode: 200,
headers : {...headers, 'Content-type': MimeType.json},
body: JSON.stringify(json, replacer)
}
}
function empty(headers: Dictionary<string> = {}): HttpResponse
{
return {
statusCode: 200,
headers,
body: undefined
}
}
function ok(body: Maybe<Buffer | string>, headers: Dictionary<string> = {}): HttpResponse
{
return {
statusCode: 200,
headers,
body
}
}
async function file(localRootPath: string, urlPath: string, headers: Dictionary<string> = {}, defaultPath = "/"): Promise<HttpResponse>
{
if (urlPath.includes('..'))
return HTTP.forbidden();
const localPath = localRootPath + (urlPath === "/" ? defaultPath : urlPath);
const body = await readFile(localPath).catch(_ => undefined)
if (isUndefined(body))
return HTTP.notFound();
if (!('Content-type' in headers))
{
headers = {...headers, ...contentTypeHeader(MimeType.guessFromPath(localPath))}
}
return HTTP.ok(body, headers)
}
function createServer(serve: (request: IncomingMessage) => Promise<HttpResponse>)
{
async function wrapServe(request: IncomingMessage, response: ServerResponse): Promise<void>
{
const r = await serve(request)
entries(r.headers).forEach(([k, v]) => response.setHeader(k, v as any))
response.statusCode = r.statusCode
if (isDefined(r.body))
response.end(r.body)
else
response.end()
}
return http.createServer(wrapServe);
}
const HTTP =
{
contentTypeHeader,
forbidden,
notFound,
ok,
json,
empty,
file,
createServer
}
export default HTTP;

View File

@ -0,0 +1,21 @@
// 0. Import Module
import {IEnumerable, initializeLinq} from "linq-to-typescript"
// 1. Declare that the JS types implement the IEnumerable interface
declare global {
interface Array<T> extends IEnumerable<T> { }
interface Uint8Array extends IEnumerable<number> { }
interface Uint8ClampedArray extends IEnumerable<number> { }
interface Uint16Array extends IEnumerable<number> { }
interface Uint32Array extends IEnumerable<number> { }
interface Int8Array extends IEnumerable<number> { }
interface Int16Array extends IEnumerable<number> { }
interface Int32Array extends IEnumerable<number> { }
interface Float32Array extends IEnumerable<number> { }
interface Float64Array extends IEnumerable<number> { }
interface Map<K, V> extends IEnumerable<[K, V]> { }
interface Set<T> extends IEnumerable<T> { }
interface String extends IEnumerable<string> { }
}
// 2. Bind Linq Functions to Array, Map, etc
initializeLinq()

View File

@ -0,0 +1,11 @@
let subsystemPadding = 0
export function getLogger(subsystem: string): (msg: string) => void
{
subsystemPadding = Math.max(subsystem.length, subsystemPadding)
// eslint-disable-next-line no-console
return (msg: string) => console.log(`${new Date().toLocaleString()} | ${(subsystem.padEnd(subsystemPadding))} | ${msg}`);
}

View File

@ -0,0 +1,176 @@
import {Dictionary, Func, Normalize1} from "./utilityTypes";
import {isUndefined} from "./maybe";
import {UnionToIntersection} from "simplytyped";
import {current} from "immer";
import { keys, valueToFunction } from "./utils";
// Type Compatibility
// https://www.typescriptlang.org/docs/handbook/type-compatibility.html
//TODO: review
export type IsUnionCase<T> =
T extends Dictionary
? [UnionToIntersection<keyof T>] extends [keyof T]
? [keyof T] extends [UnionToIntersection<keyof T>]
? true
: false
: false
: false
//TODO: review
export type IsTaggedUnion<T> = true extends UnionToIntersection<IsUnionCase<T>> ? Dictionary : never
export type Unwrap<U extends IsTaggedUnion<U>> = UnionToIntersection<U>[keyof UnionToIntersection<U>] ;
export function update<U extends IsTaggedUnion<U>>(u: U, e: Partial<Unwrap<U>>)
{
const v = u as UnionToIntersection<U>
const o = current(v)
const ks = keys(v)
if (ks.length != 1)
throw new Error("not a valid union case")
const tag = ks[0]
const before = v[tag];
const before2 = current(before);
const newVar = {...before, ...e};
v[tag] = newVar
}
export function unwrap<U extends IsTaggedUnion<U>>(u: U) : Normalize1<Unwrap<U>>
{
const v = u as UnionToIntersection<U>
const ks = keys(v)
if (ks.length != 1)
throw new Error("not a valid union case")
const key = ks[0]
return v[key] as any;
}
export function base<U extends IsTaggedUnion<U>>(u: U): Normalize1<Unwrap<U> & Partial<UnionToIntersection<Unwrap<U>>>>
{
return unwrap(u) as Normalize1<Unwrap<U> & Partial<UnionToIntersection<Unwrap<U>>>>
}
export function tag<U extends IsTaggedUnion<U>>(u: U) : keyof UnionToIntersection<U>
{
const v = u as UnionToIntersection<U>
const ks = keys(v)
if (ks.length != 1)
throw new Error("not a valid union case")
return ks[0]
}
export function tagsEqual<U extends IsTaggedUnion<U>>(u: U, v: U) : v is U
{
return tag(u) === tag(v)
}
type MapFuncs<U> = { [k in keyof UnionToIntersection<U>]: Func<UnionToIntersection<U>[k]> }
type OtherwiseKeys<U,M> = Exclude<keyof UnionToIntersection<U>, keyof M>;
type OtherwiseArg<U,M> = {
[k in keyof UnionToIntersection<U>]: Record<k, UnionToIntersection<U>[k]>
}[OtherwiseKeys<U,M>]
type OtherwiseFunc<U, M extends Partial<MapFuncs<U>>, R> = Func<OtherwiseArg<U,M> extends never ? unknown : OtherwiseArg<U,M>, R>;
export function match<U extends IsTaggedUnion<U>, M extends Partial<MapFuncs<U>>, R>(uCase: U, matchFuncs: M, otherwise: OtherwiseFunc<U, M, R> | R):{ [k in keyof M]: M[k] extends Func<any, infer O> ? O : never }[keyof M] | R
{
const otw = valueToFunction(otherwise)
const c = uCase as UnionToIntersection<U>
const ks = keys(c)
if (ks.length != 1)
return otw(c)
const key = ks[0]
const arg = c[key]
const matchFunc = matchFuncs[key]
if (isUndefined(matchFunc))
return otw(c);
return matchFunc(arg as any) as any;
}
export function dispatch<U extends IsTaggedUnion<U>>()
{
// type Intersection = UnionToIntersection<U>;
//
// type MapFuncs = { [k in keyof Intersection]: Func<Intersection[k]> }
// type OtherwiseKeys<M> = Exclude<keyof Intersection, keyof M>;
//
// type OtherwiseArg<M> = {
// [k in keyof Intersection]: Record<k, Intersection[k]>
// }[OtherwiseKeys<M>]
//
// type OtherwiseFunc<M extends Partial<MapFuncs>, R> = Func<OtherwiseArg<M> extends never ? unknown : OtherwiseArg<M>, R>;
return <M extends Partial<MapFuncs<U>>, R>(matchFuncs: M, otherwise: OtherwiseFunc<U,M, R> | R) =>
{
const otw = valueToFunction(otherwise)
return (uCase: U): { [k in keyof M]: M[k] extends Func<any, infer O> ? O : never }[keyof M] | R =>
{
const c = uCase as UnionToIntersection<U>
const ks = keys(c)
if (ks.length != 1)
return otw(c)
const key = ks[0]
const arg = c[key]
const matchFunc = matchFuncs[key]
if (isUndefined(matchFunc))
return otw(c);
return matchFunc(arg as any) as any;
}
};
}
export function concat<R extends Record<keyof any, any>, T extends Dictionary>(rec: R, t:T)
{
const result = {} as {
[k in keyof UnionToIntersection<R>]: Record<k, UnionToIntersection<R>[k] & T>
}[keyof UnionToIntersection<R>]
for (const k in rec)
{
// @ts-ignore
result[k] = { ...rec[k], ...t}
}
return result
}

View File

@ -0,0 +1,16 @@
export type Maybe<T> = T | undefined | null;
export function isDefined<T>(e: Maybe<T>): e is T
{
return e != undefined // != by design to include null
}
export function isUndefined<T>(e: Maybe<T>): e is undefined | null
{
return e == undefined // == by design to include null
}
export function toArray<T>(e: Maybe<T>): T[]
{
return isDefined(e) ? [e] : []
}

View File

@ -0,0 +1,18 @@
export type Milliseconds = number
export const Milliseconds =
{
fromSeconds: (count: number): Milliseconds => count * 1000,
fromMinutes: (count: number): Milliseconds => count * 1000 * 60,
fromHours : (count: number): Milliseconds => count * 1000 * 60 * 60,
fromDays : (count: number): Milliseconds => count * 1000 * 60 * 60 * 24,
fromWeeks : (count: number): Milliseconds => count * 1000 * 60 * 60 * 24 * 7,
toSeconds: (count: Milliseconds): number => count / 1000,
toMinutes: (count: Milliseconds): number => count / 1000 / 60,
toHours : (count: Milliseconds): number => count / 1000 / 60 / 60,
toDays : (count: Milliseconds): number => count / 1000 / 60 / 60 / 24,
toWeeks : (count: Milliseconds): number => count / 1000 / 60 / 60 / 24 / 7,
} as const

View File

@ -0,0 +1,32 @@
import PlatformPath from "path";
import { isDefined } from "./maybe";
function guessFromPath(path: string) : string
{
const ext = PlatformPath.parse(path).ext?.substring(1) as keyof typeof MimeType;
const mimeType = MimeType[ext]
return isDefined(mimeType) && typeof mimeType === "string"
? mimeType
: 'application/octet-stream'
}
const MimeType =
{
ico : 'image/x-icon',
html: 'text/html; charset=UTF-8',
js : 'text/javascript',
json: 'application/json; charset=UTF-8',
css : 'text/css; charset=UTF-8',
png : 'image/png',
jpg : 'image/jpeg',
wav : 'audio/wav',
mp3 : 'audio/mpeg',
svg : 'image/svg+xml; charset=UTF-8',
pdf : 'application/pdf',
guessFromPath
};
export default MimeType

View File

@ -0,0 +1,81 @@
import {isUndefined} from "./maybe";
import {from, IEnumerable} from "linq-to-typescript";
import {isBoolean, isNumber, isPlainObject, isString} from "./runtimeTypeChecking";
function getAt(root: any, path: (keyof any)[])
{
return path.reduce((v, p) => v[p], root)
}
function iterate(root: unknown): IEnumerable<{ path: string[]; node: unknown }>
{
if (isUndefined(root))
return []
return from(iterate(root))
function* iterate(node: unknown, path: string[] = []): Generator<{ path: string[]; node: unknown }>
{
if (isString(node) || isNumber(node) || isBoolean(node))
yield {path, node}
else if (isPlainObject(node))
for (const key in node)
{
path.push(key)
yield {path, node}
yield* iterate(node[key], path)
path.pop()
}
}
}
function iterateLeafs(root: unknown): IEnumerable<{ path: string[]; node: unknown }>
{
if (isUndefined(root))
return []
return from(iterate(root))
function* iterate(node: unknown, path: string[] = []): Generator<{ path: string[]; node: unknown }>
{
if (isString(node) || isNumber(node) || isBoolean(node))
yield {path, node}
else if (isPlainObject(node))
for (const key in node)
{
path.push(key)
yield* iterate(node[key], path)
path.pop()
}
}
}
function iterateBranches(root: unknown): IEnumerable<{ path: string[]; node: unknown }>
{
if (isUndefined(root))
return []
return from(iterate(root))
function* iterate(node: unknown, path: string[] = []): Generator<{ path: string[]; node: unknown }>
{
if (isPlainObject(node))
for (const key in node)
{
path.push(key)
yield {path, node}
yield* iterate(node[key], path)
path.pop()
}
}
}
export const Path =
{
iterate,
iterateLeafs,
iterateBranches,
getAt
} as const

View File

@ -0,0 +1,45 @@
import {IncomingMessage} from "http";
import {firstValueFrom, map, Observable, startWith, toArray} from "rxjs";
export function observeData(request: IncomingMessage, maxLength: number = Number.POSITIVE_INFINITY): Observable<Uint8Array>
{
let nBytes = 0;
return new Observable<Uint8Array>(subscriber =>
{
request.on('end', () => subscriber.complete());
request.on('data', (data: Uint8Array) =>
{
nBytes += data.byteLength
if (nBytes <= maxLength)
subscriber.next(data);
else
{
const error = `too much data: expected ${maxLength} bytes or less, got ${nBytes} bytes.`;
subscriber.error(error);
request.destroy(new Error(error))
}
});
});
}
export async function getRequestJson<T = unknown>(request: IncomingMessage, maxLength = 500000): Promise<T>
{
const data = await getData(request, maxLength)
return JSON.parse(data.toString())
}
const noData = new Uint8Array(0);
export function getData(request: IncomingMessage, maxLength: number = Number.POSITIVE_INFINITY): Promise<Buffer>
{
const data = observeData(request, maxLength).pipe
(
startWith(noData),
toArray(),
map(b => Buffer.concat(b)), // cannot inline!
)
return firstValueFrom(data);
}

View File

@ -0,0 +1,66 @@
export type TypeCode =
| "undefined"
| "object"
| "boolean"
| "number"
| "string"
| "function"
| "symbol"
| "bigint";
export type PlainObject<K extends keyof any = keyof any, V = unknown> = Record<K, V>
export function isObject(thing: unknown) : thing is object
{
return typeof thing === "object"
}
export function isDate(thing: unknown) : thing is Date
{
return thing instanceof Date
}
export function isPlainObject(thing: unknown) : thing is PlainObject
{
return isObject(thing) && !isDate(thing)
}
export function isArray(thing: unknown) : thing is Array<unknown>
{
return Array.isArray(thing)
}
export function isNumber(thing: unknown) : thing is number
{
return typeof thing === "number"
}
export function isBoolean(thing: unknown) : thing is boolean
{
return typeof thing === "boolean"
}
export function isString(thing: unknown) : thing is string
{
return typeof thing === "string"
}
// export function isFunction(thing: unknown): thing is (...args: unknown[]) => unknown
// {
// return typeof thing === "function"
// }
export function isFunction(obj: unknown): obj is (...args: any[]) => any
{
return obj instanceof Function;
}
export function isSymbol(thing: unknown) : thing is symbol
{
return typeof thing === "symbol"
}
export function isBigint(thing: unknown) : thing is bigint
{
return typeof thing === "bigint"
}

View File

@ -0,0 +1,22 @@
export function toLowercaseAscii(string: string)
{
return string
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase();
}
export function containsIgnoringAccents(string: string, substring: string)
{
if (substring === "") return true;
if (string === "") return false;
substring = "" + substring;
if (substring.length > string.length)
return false;
return toLowercaseAscii(string).includes(toLowercaseAscii(substring));
}

View File

@ -0,0 +1,71 @@
import {from, IEnumerable} from "linq-to-typescript";
import {isDefined, isUndefined, Maybe} from "./maybe";
export function Tree<T>(getChildren: (t: T) => IEnumerable<T>)
{
function iterate(root: Maybe<T>): IEnumerable<T>
{
if (isUndefined(root))
return []
return from(iterateTree())
function* iterateTree()
{
const queue: T[] = [root!]
do
{
const element = queue.shift()!
yield element
for (const child of getChildren(element))
queue.push(child)
}
while (queue.length > 0)
}
}
function iterateWithPath(root: Maybe<T>): IEnumerable<T[]>
{
return isDefined(root)
? from(iterateTreeWithPath())
: [];
function* iterateTreeWithPath()
{
const stack: Array<Array<T>> = [[root!]]
while (true)
{
const head = stack[0];
if (head.length > 0)
{
yield stack
.select(l => l[0])
.toArray()
const children = getChildren(head[0]).toArray()
stack.unshift(children)
}
else
{
stack.shift() // remove empty array in front
if(stack.length > 0)
stack[0].shift()
else
break;
}
}
}
}
return {
iterate,
iterateWithPath
} as const
}

View File

@ -0,0 +1,115 @@
export {}
// export type Type =
// | "number"
// | "object"
// | "string"
// | "never"
// | "any"
// | "unknown"
// | "undefined"
// | "boolean"
// | "bigint"
// | "symbol"
// | Property[]
// | Func
//
// export type Key = "string" | "number" | "symbol"
//
// export type Property = Func |
// {
// key: Key,
// type: Type,
// readonly? : boolean,
// nullable? : boolean
// }
//
// export type Arg =
// {
// name: string,
// type: Type,
// nullable? : boolean
// }
//
// export type Func =
// {
// args: Arg[],
// returnType: Type,
// }
//
//
// type X = Partial<any>
//
// export function render(t: Type, indent = 0)
// {
// if (typeof t === "string")
// return t
//
//
// return "ERROR"
// }
//
type DeviceType =
| "Pv"
| "Load"
| "Battery"
| "Grid"
| "Inverter"
| "AcInToAcOut"
| "DcDc"
| "AcInBus"
| "AcOutBus"
| "DcBus"
| "Dc48Bus" // low voltage DC Bus, to be eliminated in later versions
type Phase =
{
voltage : number // U, non-negative
current : number // I, sign depends on device type, see sign convention below
}
type AcPhase = Phase &
{
phi : number // [0,2pi)
}
type Device =
{
Type: DeviceType,
Name?: string,
}
type Stack =
{
Top? : Device[], // 0 to N
Right? : Device // 0 or 1
Bottom? : Device[] // 0 to N
Disconnected?: boolean // not present = false
}
/// A DC device must have a field denoting its DC connection
type DcDevice = Device &
{
Dc : Phase
}
/// An AC device can have 1 to 3 AC phases
/// An AC device also needs a Frequency measurement
/// Total power can be obtained by summing the power of the phases
type AcDevice = Device &
{
Ac: AcPhase[]
Frequency: number
}
/// A low voltage 48V DC device
/// Needed to distinguish the two sides of the DCDC
/// Will be dropped once we get HV batteries
type Dc48Device = Device &
{
dc48 : Phase
}

View File

@ -0,0 +1,52 @@
import {UnionToIntersection} from "simplytyped";
export type Dictionary<T = unknown> = Record<string, T>
export type Nothing = Dictionary<never>
export type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
export type UnionToDeepPartialIntersection<U> = DeepPartial<UnionToIntersection<U>>
export type UnionToPartialIntersection<U> = Partial<UnionToIntersection<U>>
export type Func<T = unknown, R = unknown> = (arg: T) => R
export type AsyncFunc<T = unknown, R = unknown> = (arg: T) => Promise<R>
export type SyncAction<T> = (arg: T) => void
export type AsyncAction<T> = (arg: T) => Promise<void>
export type Action<T> = SyncAction<T> | AsyncAction<T>
export type Lazy<T> = () => T
export type Base64 = string
export type ValueOf<T> = T[keyof T];
export type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]>; } : T;
export type DeepMutable<T> = { -readonly [P in keyof T]: DeepMutable<T[P]> };
export type Mutable<T> = { -readonly [P in keyof T]: T[P] };
export type NumberLiteralToStringLiteral<T> = T extends number ? `${T}` : T
export type KeyedChildren<T> = { children?: Dictionary<T> }
export type Union<K extends string, V extends string = string> = { [S in K] : V}
export type IntersectionToUnion<T extends Dictionary> = { [Prop in keyof T]: Record<Prop, T[Prop]> }[keyof T] // not sure if this is aptly named
// helper to flatten (instantiate) types in editor popups
// eslint-disable-next-line @typescript-eslint/ban-types
export type Normalize<T> = T extends (...args: infer A) => infer R ? (...args: Normalize<A>) => Normalize<R>
: [T] extends [any] ? { [K in keyof T]: Normalize<T[K]> }
: never
export type Normalize1<T> = T extends (...args: infer A) => infer R ? (...args: A) => R
: [T] extends [any] ? { [K in keyof T]: T[K] }
: T
export type Normalize2<T> = T extends (...args: infer A) => infer R ? (...args: Normalize1<A>) => Normalize1<R>
: [T] extends [any] ? { [K in keyof T]: Normalize1<T[K]> }
: never
export function mutable<T>(t: T)
{
return t as Mutable<T>
}

View File

@ -0,0 +1,49 @@
import { IncomingMessage } from 'http';
import { from } from 'linq-to-typescript';
import { isUndefined, Maybe } from './maybe';
import { Dictionary, Func } from './utilityTypes';
type StringValued<T> = {
[Key in keyof T]: T[Key] extends number
? Maybe<string>
: T[Key] extends string
? Maybe<string>
: T[Key] extends boolean
? Maybe<string>
: never;
};
export function getQueryParams<T>(
request: IncomingMessage
): Maybe<StringValued<T>> {
if (isUndefined(request.url)) return undefined;
const url = new URL(request.url, `https://${request.headers.host}/`);
const query: Dictionary = {};
const urlSearchParams = new URLSearchParams(url.search);
if (!from(urlSearchParams.entries()).any()) return undefined;
for (const [key, value] of urlSearchParams.entries()) query[key] = value;
return query as StringValued<T>;
}
export function getPath(req: IncomingMessage) {
return new URL(req.url!, `https://${req.headers.host}/`).pathname;
}
export function entries<T>(t: T) {
return Object.entries(t as Dictionary);
}
export function keys<T>(t: T): (keyof T)[] {
return Object.keys(t as unknown as object) as (keyof T)[];
}
export function valueToFunction<T, R>(tr: Func<T, R> | R): Func<T, R> {
if (typeof tr === 'function') return tr as Func<T, R>;
return (_: T) => tr;
}

View File

@ -0,0 +1,27 @@
import ReactDOM from 'react-dom';
import { HelmetProvider } from 'react-helmet-async';
import { BrowserRouter } from 'react-router-dom';
import 'nprogress/nprogress.css';
import App from 'src/App';
import { SidebarProvider } from 'src/contexts/SidebarContext';
import * as serviceWorker from 'src/serviceWorker';
import UserContextProvider from './contexts/userContext';
import TokenContextProvider from './contexts/tokenContext';
ReactDOM.render(
<HelmetProvider>
<SidebarProvider>
<BrowserRouter>
<UserContextProvider>
<TokenContextProvider>
<App />
</TokenContextProvider>
</UserContextProvider>
</BrowserRouter>
</SidebarProvider>
</HelmetProvider>,
document.getElementById('root')
);
serviceWorker.unregister();

View File

@ -0,0 +1,30 @@
import { I_S3Credentials } from 'src/interfaces/S3Types';
export interface I_Installation extends I_S3Credentials {
type: string;
title?: string;
status?: number;
detail?: string;
instance?: string;
location: string;
region: string;
country: string;
orderNumbers: string;
lat: number;
long: number;
id: number;
name: string;
information: string;
parentId: number;
s3WriteKey: string;
s3WriteSecret: string;
}
export interface I_Folder {
id: number;
name: string;
information: string;
parentId: number;
type: string;
children?: (I_Installation | I_Folder)[];
}

View File

@ -0,0 +1,12 @@
export interface I_S3Credentials {
s3Region: string;
s3Provider: string;
s3Key: string;
s3Secret: string;
s3Bucket?: string;
}
export interface Notification {
key: string;
value: string;
}

View File

@ -0,0 +1,19 @@
export type InnovEnergyUser = {
email: string;
hasWriteAccess: boolean;
id: number;
information: string;
language: string;
name: string;
parentId: number;
password: number;
type: string;
folderIds?: number[];
mustResetPassword: boolean;
};
export interface I_UserWithInheritedAccess {
folderId: number;
folderName: string;
user: InnovEnergyUser;
}

View File

@ -43,5 +43,10 @@
"updatedSuccessfully": "Erfolgreich aktualisiert",
"user": "Nutzer",
"userTabs": "Nutzer",
"users": "Nutzer"
"users": "Nutzer",
"status": "Status",
"live": "Live Übertragung",
"deleteInstallation": "Installation löschen",
"errorOccured": "Ein Fehler ist aufgetreten",
"successfullyUpdated": "Erfolgreich aktualisiert"
}

View File

@ -48,5 +48,10 @@
"requiredOrderNumber": "Required Order Number",
"submit": "Submit",
"user": "User",
"userTabs": "user tabs"
"userTabs": "user tabs",
"status": "Status",
"live": "Live View",
"deleteInstallation": "Delete Installation",
"errorOccured": "An error has occurred",
"successfullyUpdated": "Successfully updated"
}

Some files were not shown because too many files have changed in this diff Show More