updated frontend
This commit is contained in:
parent
dded09c595
commit
13fabb8e16
|
@ -0,0 +1,4 @@
|
||||||
|
# .estlintignore file
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
node_modules/
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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*
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"bracketSameLine": false
|
||||||
|
}
|
|
@ -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.
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 |
|
@ -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 |
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
|
@ -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;
|
|
@ -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">
|
||||||
|
© 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||||
|
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||||
|
don’t 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;
|
|
@ -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 & 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 & perfect apps development processes.
|
||||||
|
</TypographySecondary>
|
||||||
|
<Button href="/overview" size="large" variant="contained">
|
||||||
|
Overview
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</GridWrapper>
|
||||||
|
</Hidden>
|
||||||
|
</Grid>
|
||||||
|
</MainContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Status500;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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}`;
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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('')
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
}
|
|
@ -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
|
||||||
|
// }
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export function trim(str: string, string: string = " "): string
|
||||||
|
{
|
||||||
|
const pattern = '^[' + string + ']*(.*?)[' + string + ']*$';
|
||||||
|
return str.replace(new RegExp(pattern), '$1')
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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] }
|
|
@ -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));
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -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()
|
|
@ -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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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] : []
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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();
|
|
@ -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)[];
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { FC, ReactNode } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
|
interface BaseLayoutProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseLayout: FC<BaseLayoutProps> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
height: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children || <Outlet />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BaseLayout.propTypes = {
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BaseLayout;
|
|
@ -0,0 +1,128 @@
|
||||||
|
import {
|
||||||
|
alpha,
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
Popover,
|
||||||
|
Tooltip,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
|
||||||
|
import { formatDistance, subDays } from 'date-fns';
|
||||||
|
|
||||||
|
const NotificationsBadge = styled(Badge)(
|
||||||
|
({ theme }) => `
|
||||||
|
|
||||||
|
.MuiBadge-badge {
|
||||||
|
background-color: ${alpha(theme.palette.error.main, 0.1)};
|
||||||
|
color: ${theme.palette.error.main};
|
||||||
|
min-width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 0 1px ${alpha(theme.palette.error.main, 0.3)};
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
interface LanguageComponentProps {
|
||||||
|
language: string;
|
||||||
|
onSelectLanguage: (item: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeaderNotifications() {
|
||||||
|
const ref = useRef<any>(null);
|
||||||
|
const [isOpen, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleOpen = (): void => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip arrow title="Notifications">
|
||||||
|
<IconButton color="primary" ref={ref} onClick={handleOpen}>
|
||||||
|
<NotificationsBadge
|
||||||
|
badgeContent={1}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NotificationsActiveTwoToneIcon />
|
||||||
|
</NotificationsBadge>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Popover
|
||||||
|
anchorEl={ref.current}
|
||||||
|
onClose={handleClose}
|
||||||
|
open={isOpen}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{ p: 2 }}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Typography variant="h5">Notifications</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<List sx={{ p: 0 }}>
|
||||||
|
<ListItem
|
||||||
|
sx={{ p: 2, minWidth: 350, display: { xs: 'block', sm: 'flex' } }}
|
||||||
|
>
|
||||||
|
<Box flex="1">
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Typography sx={{ fontWeight: 'bold' }}>
|
||||||
|
Messaging Platform
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ textTransform: 'none' }}>
|
||||||
|
{formatDistance(subDays(new Date(), 3), new Date(), {
|
||||||
|
addSuffix: true
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
new messages in your inbox
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeaderNotifications;
|
|
@ -0,0 +1,279 @@
|
||||||
|
import { forwardRef, Ref, useState, ReactElement, ChangeEvent } from 'react';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Link,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
lighten,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
TextField,
|
||||||
|
Theme,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Slide,
|
||||||
|
Hidden
|
||||||
|
} from '@mui/material';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { TransitionProps } from '@mui/material/transitions';
|
||||||
|
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
|
||||||
|
import FindInPageTwoToneIcon from '@mui/icons-material/FindInPageTwoTone';
|
||||||
|
|
||||||
|
import ChevronRightTwoToneIcon from '@mui/icons-material/ChevronRightTwoTone';
|
||||||
|
|
||||||
|
const Transition = forwardRef(function Transition(
|
||||||
|
props: TransitionProps & { children: ReactElement<any, any> },
|
||||||
|
ref: Ref<unknown>
|
||||||
|
) {
|
||||||
|
return <Slide direction="down" ref={ref} {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const DialogWrapper = styled(Dialog)(
|
||||||
|
() => `
|
||||||
|
.MuiDialog-container {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiDialog-paperScrollPaper {
|
||||||
|
max-height: calc(100vh - 64px)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const SearchInputWrapper = styled(TextField)(
|
||||||
|
({ theme }) => `
|
||||||
|
background: ${theme.colors.alpha.white[100]};
|
||||||
|
|
||||||
|
.MuiInputBase-input {
|
||||||
|
font-size: ${theme.typography.pxToRem(17)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const DialogTitleWrapper = styled(DialogTitle)(
|
||||||
|
({ theme }) => `
|
||||||
|
background: ${theme.colors.alpha.black[5]};
|
||||||
|
padding: ${theme.spacing(3)}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
function HeaderSearch() {
|
||||||
|
const [openSearchResults, setOpenSearchResults] = useState(false);
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
|
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setSearchValue(event.target.value);
|
||||||
|
|
||||||
|
if (event.target.value) {
|
||||||
|
if (!openSearchResults) {
|
||||||
|
setOpenSearchResults(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setOpenSearchResults(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleClickOpen = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip arrow title="Search">
|
||||||
|
<IconButton color="primary" onClick={handleClickOpen}>
|
||||||
|
<SearchTwoToneIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<DialogWrapper
|
||||||
|
open={open}
|
||||||
|
TransitionComponent={Transition}
|
||||||
|
keepMounted
|
||||||
|
maxWidth="md"
|
||||||
|
fullWidth
|
||||||
|
scroll="paper"
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<DialogTitleWrapper>
|
||||||
|
<SearchInputWrapper
|
||||||
|
value={searchValue}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchTwoToneIcon />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
placeholder="Search terms here..."
|
||||||
|
fullWidth
|
||||||
|
label="Search"
|
||||||
|
/>
|
||||||
|
</DialogTitleWrapper>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{openSearchResults && (
|
||||||
|
<DialogContent>
|
||||||
|
<Box
|
||||||
|
sx={{ pt: 0, pb: 1 }}
|
||||||
|
display="flex"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Typography variant="body2" component="span">
|
||||||
|
Search results for{' '}
|
||||||
|
<Typography
|
||||||
|
sx={{ fontWeight: 'bold' }}
|
||||||
|
variant="body1"
|
||||||
|
component="span"
|
||||||
|
>
|
||||||
|
{searchValue}
|
||||||
|
</Typography>
|
||||||
|
</Typography>
|
||||||
|
<Link href="#" variant="body2" underline="hover">
|
||||||
|
Advanced search
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
<List disablePadding>
|
||||||
|
<ListItem button>
|
||||||
|
<Hidden smDown>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
background: (theme: Theme) =>
|
||||||
|
theme.palette.secondary.main
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FindInPageTwoToneIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
</Hidden>
|
||||||
|
<Box flex="1">
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Link
|
||||||
|
href="#"
|
||||||
|
underline="hover"
|
||||||
|
sx={{ fontWeight: 'bold' }}
|
||||||
|
variant="body2"
|
||||||
|
>
|
||||||
|
Dashboard for Healthcare Platform
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: (theme: Theme) =>
|
||||||
|
lighten(theme.palette.secondary.main, 0.5)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
This page contains all the necessary information for
|
||||||
|
managing all hospital staff.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<ChevronRightTwoToneIcon />
|
||||||
|
</ListItem>
|
||||||
|
<Divider sx={{ my: 1 }} component="li" />
|
||||||
|
<ListItem button>
|
||||||
|
<Hidden smDown>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
background: (theme: Theme) =>
|
||||||
|
theme.palette.secondary.main
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FindInPageTwoToneIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
</Hidden>
|
||||||
|
<Box flex="1">
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Link
|
||||||
|
href="#"
|
||||||
|
underline="hover"
|
||||||
|
sx={{ fontWeight: 'bold' }}
|
||||||
|
variant="body2"
|
||||||
|
>
|
||||||
|
Example Projects Application
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: (theme: Theme) =>
|
||||||
|
lighten(theme.palette.secondary.main, 0.5)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
This is yet another search result pointing to a app page.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<ChevronRightTwoToneIcon />
|
||||||
|
</ListItem>
|
||||||
|
<Divider sx={{ my: 1 }} component="li" />
|
||||||
|
<ListItem button>
|
||||||
|
<Hidden smDown>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
background: (theme: Theme) =>
|
||||||
|
theme.palette.secondary.main
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FindInPageTwoToneIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
</Hidden>
|
||||||
|
<Box flex="1">
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Link
|
||||||
|
href="#"
|
||||||
|
underline="hover"
|
||||||
|
sx={{ fontWeight: 'bold' }}
|
||||||
|
variant="body2"
|
||||||
|
>
|
||||||
|
Search Results Page
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: (theme: Theme) =>
|
||||||
|
lighten(theme.palette.secondary.main, 0.5)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Choose if you would like to show or not this typography
|
||||||
|
section here...
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<ChevronRightTwoToneIcon />
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
<Divider sx={{ mt: 1, mb: 2 }} />
|
||||||
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
|
<Button color="primary">View all search results</Button>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
)}
|
||||||
|
</DialogWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeaderSearch;
|
|
@ -0,0 +1,148 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Switch
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useContext, useRef, useState } from 'react';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import ExpandMoreTwoToneIcon from '@mui/icons-material/ExpandMoreTwoTone';
|
||||||
|
import { ThemeContext } from '../../../../theme/ThemeProvider';
|
||||||
|
|
||||||
|
interface HeaderButtonsProps {
|
||||||
|
language: string;
|
||||||
|
onSelectLanguage: (item: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListWrapper = styled(Box)(
|
||||||
|
({ theme }) => `
|
||||||
|
.MuiTouchRipple-root {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiListItem-root {
|
||||||
|
transition: ${theme.transitions.create(['color', 'fill'])};
|
||||||
|
|
||||||
|
&.MuiListItem-indicators {
|
||||||
|
padding: ${theme.spacing(1, 2)};
|
||||||
|
|
||||||
|
.MuiListItemText-root {
|
||||||
|
.MuiTypography-root {
|
||||||
|
&:before {
|
||||||
|
height: 4px;
|
||||||
|
width: 22px;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
transition: all .2s;
|
||||||
|
border-radius: ${theme.general.borderRadiusLg};
|
||||||
|
content: "";
|
||||||
|
background: ${theme.colors.primary.main};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active,
|
||||||
|
&:active,
|
||||||
|
&:hover {
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.MuiListItemText-root {
|
||||||
|
.MuiTypography-root {
|
||||||
|
&:before {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
function HeaderMenu(props: HeaderButtonsProps) {
|
||||||
|
const ref = useRef<any>(null);
|
||||||
|
const [isOpen, setOpen] = useState<boolean>(false);
|
||||||
|
const themeContext = useContext(ThemeContext);
|
||||||
|
if (!themeContext) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const setThemeName = themeContext;
|
||||||
|
|
||||||
|
const [darkState, setDarkState] = useState(false);
|
||||||
|
|
||||||
|
const handleThemeChange = () => {
|
||||||
|
setDarkState(!darkState);
|
||||||
|
if (darkState == false) {
|
||||||
|
setThemeName('NebulaFighterTheme');
|
||||||
|
localStorage.setItem('theme', 'NebulaFighterTheme');
|
||||||
|
} else {
|
||||||
|
setThemeName('PureLightTheme');
|
||||||
|
localStorage.setItem('theme', 'PureLightTheme');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpen = (): void => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListWrapper
|
||||||
|
sx={{
|
||||||
|
display: {
|
||||||
|
xs: 'none',
|
||||||
|
md: 'block'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List disablePadding component={Box} display="flex">
|
||||||
|
<ListItem
|
||||||
|
classes={{ root: 'MuiListItem-indicators' }}
|
||||||
|
button
|
||||||
|
ref={ref}
|
||||||
|
onClick={handleOpen}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primaryTypographyProps={{ noWrap: true }}
|
||||||
|
primary={
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
Language
|
||||||
|
<Box display="flex" alignItems="center" pl={0.3}>
|
||||||
|
<ExpandMoreTwoToneIcon fontSize="small" />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</ListWrapper>
|
||||||
|
<Switch checked={darkState} onChange={handleThemeChange} />
|
||||||
|
<Menu anchorEl={ref.current} onClose={handleClose} open={isOpen}>
|
||||||
|
<MenuItem value="en" onClick={() => props.onSelectLanguage('en')}>
|
||||||
|
English
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="de" onClick={() => props.onSelectLanguage('de')}>
|
||||||
|
German
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="fr" onClick={() => props.onSelectLanguage('fr')}>
|
||||||
|
French
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeaderMenu;
|
|
@ -0,0 +1,132 @@
|
||||||
|
import React, { useContext, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Hidden,
|
||||||
|
lighten,
|
||||||
|
Popover,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import ExpandMoreTwoToneIcon from '@mui/icons-material/ExpandMoreTwoTone';
|
||||||
|
import LockOpenTwoToneIcon from '@mui/icons-material/LockOpenTwoTone';
|
||||||
|
import { UserContext } from '../../../../contexts/userContext';
|
||||||
|
import axiosConfig from 'src/Resources/axiosConfig';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { TokenContext } from 'src/contexts/tokenContext';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const UserBoxButton = styled(Button)(
|
||||||
|
({ theme }) => `
|
||||||
|
padding-left: ${theme.spacing(1)};
|
||||||
|
padding-right: ${theme.spacing(1)};
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const MenuUserBox = styled(Box)(
|
||||||
|
({ theme }) => `
|
||||||
|
background: ${theme.colors.alpha.black[5]};
|
||||||
|
padding: ${theme.spacing(2)};
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const UserBoxText = styled(Box)(
|
||||||
|
({ theme }) => `
|
||||||
|
text-align: left;
|
||||||
|
padding-left: ${theme.spacing(1)};
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const UserBoxLabel = styled(Typography)(
|
||||||
|
({ theme }) => `
|
||||||
|
font-weight: ${theme.typography.fontWeightBold};
|
||||||
|
color: ${theme.palette.secondary.main};
|
||||||
|
display: block;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const UserBoxDescription = styled(Typography)(
|
||||||
|
({ theme }) => `
|
||||||
|
color: ${lighten(theme.palette.secondary.main, 0.5)}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
function HeaderUserbox() {
|
||||||
|
const ref = useRef<any>(null);
|
||||||
|
const [isOpen, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const context = useContext(UserContext);
|
||||||
|
const { currentUser, setUser, removeUser } = context;
|
||||||
|
const tokencontext = useContext(TokenContext);
|
||||||
|
const { token, setNewToken, removeToken } = tokencontext;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
axiosConfig.post('/Logout').then(() => {
|
||||||
|
removeToken();
|
||||||
|
removeUser();
|
||||||
|
localStorage.removeItem('theme');
|
||||||
|
navigate('/');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpen = (): void => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UserBoxButton color="secondary" ref={ref} onClick={handleOpen}>
|
||||||
|
<Hidden mdDown>
|
||||||
|
<UserBoxText>
|
||||||
|
<UserBoxLabel variant="body1">{currentUser?.name}</UserBoxLabel>
|
||||||
|
<UserBoxDescription variant="body2">
|
||||||
|
{currentUser?.email}
|
||||||
|
</UserBoxDescription>
|
||||||
|
</UserBoxText>
|
||||||
|
</Hidden>
|
||||||
|
<Hidden smDown>
|
||||||
|
<ExpandMoreTwoToneIcon sx={{ ml: 1 }} />
|
||||||
|
</Hidden>
|
||||||
|
</UserBoxButton>
|
||||||
|
<Popover
|
||||||
|
anchorEl={ref.current}
|
||||||
|
onClose={handleClose}
|
||||||
|
open={isOpen}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuUserBox sx={{ minWidth: 210 }} display="flex">
|
||||||
|
<UserBoxText>
|
||||||
|
<UserBoxLabel variant="body1">{currentUser?.name}</UserBoxLabel>
|
||||||
|
<UserBoxDescription variant="body2">
|
||||||
|
{currentUser?.email}
|
||||||
|
</UserBoxDescription>
|
||||||
|
</UserBoxText>
|
||||||
|
</MenuUserBox>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ m: 1 }}>
|
||||||
|
<Button color="primary" fullWidth onClick={handleSubmit}>
|
||||||
|
<LockOpenTwoToneIcon sx={{ mr: 1 }} />
|
||||||
|
<FormattedMessage id="logout" defaultMessage="Logout" />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeaderUserbox;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue