mirror of
https://github.com/Telecominfraproject/wlan-cloud-ucentral-ui-libs.git
synced 2025-10-30 02:12:22 +00:00
Initial commit
This commit is contained in:
19
.editorconfig
Normal file
19
.editorconfig
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 100
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
[*.md]
|
||||||
|
max_line_length = 0
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
[{Makefile,**.mk}]
|
||||||
|
# Use tabs for indentation (Makefiles require tabs)
|
||||||
|
indent_style = tab
|
||||||
|
[*.scss]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/src/assets
|
||||||
|
/build
|
||||||
|
/node_modules
|
||||||
|
.github
|
||||||
34
.eslintrc
Normal file
34
.eslintrc
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"extends": ["airbnb", "prettier"],
|
||||||
|
"plugins": ["prettier"],
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"max-len": ["error", {"code": 150}],
|
||||||
|
"prefer-promise-reject-errors": ["off"],
|
||||||
|
"react/jsx-filename-extension": ["off"],
|
||||||
|
"react/prop-types": ["warn"],
|
||||||
|
"no-return-assign": ["off"],
|
||||||
|
"react/jsx-props-no-spreading": ["off"],
|
||||||
|
"react/destructuring-assignment": ["off"],
|
||||||
|
"no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"],
|
||||||
|
"react/jsx-one-expression-per-line": "off",
|
||||||
|
"react/jsx-wrap-multilines": "off",
|
||||||
|
"react/jsx-curly-newline": "off"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"node": {
|
||||||
|
"paths": ["src"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module",
|
||||||
|
"allowImportExportEverywhere": false,
|
||||||
|
"codeFrame": false
|
||||||
|
},
|
||||||
|
}
|
||||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/src/assets
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
.github
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
29
LICENSE
Normal file
29
LICENSE
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2021, Stephane Bourque
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
19
README.md
Normal file
19
README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# uCentral UI Library
|
||||||
|
|
||||||
|
## What is this?
|
||||||
|
The uCentral UI Library is a library that will be used for different micro services clients part of the uCentral program such as [uCentralGW UI](https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui). To use the interface,
|
||||||
|
you either need to run it on your machine for development or install it.
|
||||||
|
|
||||||
|
### Development
|
||||||
|
Here are the instructions to use the librar Please install `npm` for the platform you are using.
|
||||||
|
```
|
||||||
|
git clone https://github.com/Telecominfraproject/wlan-cloud-ucentral-ui-libs
|
||||||
|
cd wlan-cloud-ucentral-ui-libs
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install for use in other projects
|
||||||
|
```
|
||||||
|
npm install ucentral-libs
|
||||||
|
```
|
||||||
|
|
||||||
12
babel.config.json
Normal file
12
babel.config.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
"@babel/preset-react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-proposal-class-properties",
|
||||||
|
"transform-react-remove-prop-types",
|
||||||
|
"@babel/plugin-transform-react-inline-elements",
|
||||||
|
"@babel/plugin-transform-react-constant-elements"
|
||||||
|
]
|
||||||
|
}
|
||||||
9
jsconfig.json
Normal file
9
jsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src",
|
||||||
|
"paths": {
|
||||||
|
"*": ["*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
23550
package-lock.json
generated
Normal file
23550
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
85
package.json
Normal file
85
package.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"name": "ucentral-libs",
|
||||||
|
"version": "0.8.9",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"source": "src/index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"@coreui/coreui": "^3.4.0",
|
||||||
|
"@coreui/icons": "^2.0.1",
|
||||||
|
"@coreui/icons-react": "^1.1.0",
|
||||||
|
"@coreui/react": "^3.4.6",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-paginate": "^7.1.3",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --write 'src/**/*{.js,.scss}'",
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"eslint": "eslint 'src/**/*.js'",
|
||||||
|
"watch": "webpack --watch",
|
||||||
|
"fix": "eslint --fix 'src/**/*.js'"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx}": [
|
||||||
|
"eslint",
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.14.6",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||||
|
"@babel/plugin-transform-react-constant-elements": "^7.14.5",
|
||||||
|
"@babel/plugin-transform-react-inline-elements": "^7.14.5",
|
||||||
|
"@babel/polyfill": "^7.12.1",
|
||||||
|
"@babel/preset-env": "^7.14.7",
|
||||||
|
"@babel/preset-react": "^7.14.5",
|
||||||
|
"autoprefixer": "^10.2.6",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"babel-loader": "^8.2.2",
|
||||||
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
|
"babel-polyfill": "^6.26.0",
|
||||||
|
"babel-runtime": "^6.26.0",
|
||||||
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
|
"css-loader": "^5.2.6",
|
||||||
|
"dotenv-webpack": "^6.0.4",
|
||||||
|
"eslint": "^7.29.0",
|
||||||
|
"eslint-config-airbnb": "^18.2.1",
|
||||||
|
"eslint-config-prettier": "^7.2.0",
|
||||||
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
|
"eslint-loader": "^4.0.2",
|
||||||
|
"eslint-plugin-babel": "^5.3.1",
|
||||||
|
"eslint-plugin-import": "^2.23.4",
|
||||||
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
|
"eslint-plugin-react": "^7.24.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
|
"husky": "^4.3.8",
|
||||||
|
"lint-staged": "^11.0.0",
|
||||||
|
"node-sass": "^5.0.0",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"sass-loader": "^11.1.1",
|
||||||
|
"style-loader": "^2.0.0",
|
||||||
|
"webpack": "^5.40.0",
|
||||||
|
"webpack-cli": "^4.7.2",
|
||||||
|
"webpack-dev-server": "^3.11.2",
|
||||||
|
"webpack-merge": "^5.8.0",
|
||||||
|
"webpack-node-externals": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/components/LanguageSwitcher/index.js
Normal file
23
src/components/LanguageSwitcher/index.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CSelect } from '@coreui/react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const LanguageSwitcher = ({ i18n }) => (
|
||||||
|
<CSelect
|
||||||
|
custom
|
||||||
|
defaultValue={i18n.language.split('-')[0]}
|
||||||
|
onChange={(e) => i18n.changeLanguage(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="de">Deutsche</option>
|
||||||
|
<option value="es">Español</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="fr">Français</option>
|
||||||
|
<option value="pt">Portugues</option>
|
||||||
|
</CSelect>
|
||||||
|
);
|
||||||
|
|
||||||
|
LanguageSwitcher.propTypes = {
|
||||||
|
i18n: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(LanguageSwitcher);
|
||||||
197
src/components/LoginPage/index.js
Normal file
197
src/components/LoginPage/index.js
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
CButton,
|
||||||
|
CCard,
|
||||||
|
CCardBody,
|
||||||
|
CCardGroup,
|
||||||
|
CCol,
|
||||||
|
CContainer,
|
||||||
|
CForm,
|
||||||
|
CInput,
|
||||||
|
CInputGroup,
|
||||||
|
CInputGroupPrepend,
|
||||||
|
CInputGroupText,
|
||||||
|
CRow,
|
||||||
|
CPopover,
|
||||||
|
CAlert,
|
||||||
|
CInvalidFeedback,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilUser, cilLockLocked, cilLink } from '@coreui/icons';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import LanguageSwitcher from '../LanguageSwitcher';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const LoginPage = ({ t, i18n, signIn, defaultConfig, error, setHadError }) => {
|
||||||
|
const [userId, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [uCentralSecUrl, setUCentralSecUrl] = useState('');
|
||||||
|
const [emptyUsername, setEmptyUsername] = useState(false);
|
||||||
|
const [emptyPassword, setEmptyPassword] = useState(false);
|
||||||
|
const [emptyGateway, setEmptyGateway] = useState(false);
|
||||||
|
const placeholderUrl = 'uCentralSec URL (ex: https://your-url:port)';
|
||||||
|
|
||||||
|
const formValidation = () => {
|
||||||
|
setHadError(false);
|
||||||
|
|
||||||
|
let isSuccessful = true;
|
||||||
|
|
||||||
|
if (userId.trim() === '') {
|
||||||
|
setEmptyUsername(true);
|
||||||
|
isSuccessful = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.trim() === '') {
|
||||||
|
setEmptyPassword(true);
|
||||||
|
isSuccessful = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uCentralSecUrl.trim() === '') {
|
||||||
|
setEmptyGateway(true);
|
||||||
|
isSuccessful = false;
|
||||||
|
}
|
||||||
|
return isSuccessful;
|
||||||
|
};
|
||||||
|
const onKeyDown = (event) => {
|
||||||
|
if (event.code === 'Enter' && formValidation()) {
|
||||||
|
signIn({ userId, password }, uCentralSecUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (emptyUsername) setEmptyUsername(false);
|
||||||
|
}, [userId]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (emptyPassword) setEmptyPassword(false);
|
||||||
|
}, [password]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (emptyGateway) setEmptyGateway(false);
|
||||||
|
}, [uCentralSecUrl]);
|
||||||
|
useEffect(() => {
|
||||||
|
setUCentralSecUrl(defaultConfig.DEFAULT_UCENTRALSEC_URL);
|
||||||
|
}, [defaultConfig]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="c-app c-default-layout flex-row align-items-center">
|
||||||
|
<CContainer>
|
||||||
|
<CRow className="justify-content-center">
|
||||||
|
<CCol md="8">
|
||||||
|
<img
|
||||||
|
className={[styles.logo, 'c-sidebar-brand-full'].join(' ')}
|
||||||
|
src="assets/OpenWiFi_LogoLockup_DarkGreyColour.svg"
|
||||||
|
alt="OpenWifi"
|
||||||
|
/>
|
||||||
|
<CCardGroup>
|
||||||
|
<CCard className="p-4">
|
||||||
|
<CCardBody>
|
||||||
|
<CForm onKeyDown={onKeyDown}>
|
||||||
|
<h1>{t('login.login')}</h1>
|
||||||
|
<p className="text-muted">{t('login.sign_in_to_account')}</p>
|
||||||
|
<CInputGroup className="mb-3">
|
||||||
|
<CPopover content={t('login.username')}>
|
||||||
|
<CInputGroupPrepend>
|
||||||
|
<CInputGroupText>
|
||||||
|
<CIcon name="cilUser" content={cilUser} />
|
||||||
|
</CInputGroupText>
|
||||||
|
</CInputGroupPrepend>
|
||||||
|
</CPopover>
|
||||||
|
<CInput
|
||||||
|
invalid={emptyUsername}
|
||||||
|
autoFocus
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
placeholder={t('login.username')}
|
||||||
|
autoComplete="username"
|
||||||
|
onChange={(event) => setUsername(event.target.value)}
|
||||||
|
/>
|
||||||
|
<CInvalidFeedback className="help-block">
|
||||||
|
{t('login.please_enter_username')}
|
||||||
|
</CInvalidFeedback>
|
||||||
|
</CInputGroup>
|
||||||
|
<CInputGroup className="mb-4">
|
||||||
|
<CPopover content={t('login.password')}>
|
||||||
|
<CInputGroupPrepend>
|
||||||
|
<CInputGroupText>
|
||||||
|
<CIcon content={cilLockLocked} />
|
||||||
|
</CInputGroupText>
|
||||||
|
</CInputGroupPrepend>
|
||||||
|
</CPopover>
|
||||||
|
<CInput
|
||||||
|
invalid={emptyPassword}
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
placeholder={t('login.password')}
|
||||||
|
autoComplete="current-password"
|
||||||
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
|
/>
|
||||||
|
<CInvalidFeedback className="help-block">
|
||||||
|
{t('login.please_enter_password')}
|
||||||
|
</CInvalidFeedback>
|
||||||
|
</CInputGroup>
|
||||||
|
<CInputGroup className="mb-4" hidden={!defaultConfig.ALLOW_UCENTRALSEC_CHANGE}>
|
||||||
|
<CPopover content={t('login.url')}>
|
||||||
|
<CInputGroupPrepend>
|
||||||
|
<CInputGroupText>
|
||||||
|
<CIcon name="cilLink" content={cilLink} />
|
||||||
|
</CInputGroupText>
|
||||||
|
</CInputGroupPrepend>
|
||||||
|
</CPopover>
|
||||||
|
<CInput
|
||||||
|
invalid={emptyGateway}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder={placeholderUrl}
|
||||||
|
value={uCentralSecUrl}
|
||||||
|
autoComplete="gateway-url"
|
||||||
|
onChange={(event) => setUCentralSecUrl(event.target.value)}
|
||||||
|
/>
|
||||||
|
<CInvalidFeedback className="help-block">
|
||||||
|
{t('login.please_enter_gateway')}
|
||||||
|
</CInvalidFeedback>
|
||||||
|
</CInputGroup>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<CAlert show={error} color="danger">
|
||||||
|
{t('login.login_error')}
|
||||||
|
</CAlert>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CCol xs="6">
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
className="px-4"
|
||||||
|
onClick={() =>
|
||||||
|
formValidation() ? signIn({ userId, password }, uCentralSecUrl) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('login.login')}
|
||||||
|
</CButton>
|
||||||
|
</CCol>
|
||||||
|
<CCol xs="6">
|
||||||
|
<div className={styles.languageSwitcher}>
|
||||||
|
<LanguageSwitcher i18n={i18n} />
|
||||||
|
</div>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CForm>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCardGroup>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LoginPage.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
i18n: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
signIn: PropTypes.func.isRequired,
|
||||||
|
defaultConfig: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
error: PropTypes.bool.isRequired,
|
||||||
|
setHadError: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(LoginPage);
|
||||||
9
src/components/LoginPage/index.module.scss
Normal file
9
src/components/LoginPage/index.module.scss
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.logo {
|
||||||
|
padding-left: 17%;
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languageSwitcher {
|
||||||
|
float: right;
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
82
src/components/UserListTable/index.js
Normal file
82
src/components/UserListTable/index.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactPaginate from 'react-paginate';
|
||||||
|
import { CCard, CCardHeader, CSelect, CCol, CRow, CCardBody, CDataTable } from '@coreui/react';
|
||||||
|
|
||||||
|
const UserListTable = ({
|
||||||
|
t,
|
||||||
|
users,
|
||||||
|
loading,
|
||||||
|
usersPerPage,
|
||||||
|
setUsersPerPage,
|
||||||
|
pageCount,
|
||||||
|
setPage,
|
||||||
|
}) => {
|
||||||
|
const fields = [
|
||||||
|
{ key: 'id', label: t('common.config_id'), _style: { width: '5%' } },
|
||||||
|
{ key: 'name', label: t('common.config_id'), _style: { width: '5%' } },
|
||||||
|
{ key: 'email', label: t('common.config_id'), _style: { width: '5%' } },
|
||||||
|
{ key: 'userRole', label: t('common.config_id'), _style: { width: '5%' } },
|
||||||
|
{ key: 'id', label: t('common.config_id'), _style: { width: '5%' } },
|
||||||
|
{
|
||||||
|
key: 'user_actions',
|
||||||
|
label: '',
|
||||||
|
_style: { width: '3%' },
|
||||||
|
sorter: false,
|
||||||
|
filter: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader>
|
||||||
|
<CRow>
|
||||||
|
<CCol />
|
||||||
|
<CCol xs={1}>
|
||||||
|
<CSelect
|
||||||
|
custom
|
||||||
|
defaultValue={usersPerPage}
|
||||||
|
onChange={(e) => setUsersPerPage(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</CSelect>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CDataTable items={users} fields={fields} loading={loading} hover border />
|
||||||
|
<ReactPaginate
|
||||||
|
previousLabel="← Previous"
|
||||||
|
nextLabel="Next →"
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={setPage}
|
||||||
|
breakClassName="page-item"
|
||||||
|
breakLinkClassName="page-link"
|
||||||
|
containerClassName="pagination"
|
||||||
|
pageClassName="page-item"
|
||||||
|
pageLinkClassName="page-link"
|
||||||
|
previousClassName="page-item"
|
||||||
|
previousLinkClassName="page-link"
|
||||||
|
nextClassName="page-item"
|
||||||
|
nextLinkClassName="page-link"
|
||||||
|
activeClassName="active"
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UserListTable.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
users: PropTypes.arrayOf(Object).isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
usersPerPage: PropTypes.string.isRequired,
|
||||||
|
setUsersPerPage: PropTypes.func.isRequired,
|
||||||
|
pageCount: PropTypes.number.isRequired,
|
||||||
|
setPage: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(UserListTable);
|
||||||
12
src/index.js
Normal file
12
src/index.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Layout
|
||||||
|
export { default as Sidebar } from './layout/Sidebar';
|
||||||
|
export { default as Header } from './layout/Header';
|
||||||
|
export { default as Footer } from './layout/Footer';
|
||||||
|
export { default as PageContainer } from './layout/PageContainer';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
export { default as LanguageSwitcher } from './components/LanguageSwitcher';
|
||||||
|
export { default as UserListTable } from './components/UserListTable';
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
export { default as LoginPage } from './components/LoginPage';
|
||||||
23
src/layout/Footer/index.js
Normal file
23
src/layout/Footer/index.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CFooter } from '@coreui/react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const TheFooter = ({ t, version }) => (
|
||||||
|
<CFooter fixed={false}>
|
||||||
|
<div>
|
||||||
|
{t('footer.version')} {version}
|
||||||
|
</div>
|
||||||
|
<div className="mfs-auto">
|
||||||
|
<span className="mr-1">{t('footer.powered_by')}</span>
|
||||||
|
<a href="https://coreui.io/react" target="_blank" rel="noopener noreferrer">
|
||||||
|
{t('footer.coreui_for_react')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CFooter>
|
||||||
|
);
|
||||||
|
|
||||||
|
TheFooter.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
version: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
export default React.memo(TheFooter);
|
||||||
82
src/layout/Header/index.js
Normal file
82
src/layout/Header/index.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
CHeader,
|
||||||
|
CToggler,
|
||||||
|
CHeaderBrand,
|
||||||
|
CHeaderNav,
|
||||||
|
CSubheader,
|
||||||
|
CBreadcrumbRouter,
|
||||||
|
CLink,
|
||||||
|
CPopover,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilAccountLogout } from '@coreui/icons';
|
||||||
|
import LanguageSwitcher from '../../components/LanguageSwitcher';
|
||||||
|
|
||||||
|
const Header = ({ showSidebar, setShowSidebar, routes, t, i18n, logout, authToken, endpoints }) => {
|
||||||
|
const [translatedRoutes, setTranslatedRoutes] = useState(routes);
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
const val = [true, 'responsive'].includes(showSidebar) ? false : 'responsive';
|
||||||
|
setShowSidebar(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSidebarMobile = () => {
|
||||||
|
const val = [false, 'responsive'].includes(showSidebar) ? true : 'responsive';
|
||||||
|
setShowSidebar(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTranslatedRoutes(routes.map(({ name, ...rest }) => ({ ...rest, name: t(name) })));
|
||||||
|
}, [i18n.language]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CHeader withSubheader>
|
||||||
|
<CToggler inHeader className="ml-md-3 d-lg-none" onClick={toggleSidebarMobile} />
|
||||||
|
<CToggler inHeader className="ml-3 d-md-down-none" onClick={toggleSidebar} />
|
||||||
|
<CHeaderBrand className="mx-auto d-lg-none" to="/">
|
||||||
|
<CIcon name="logo" height="48" alt="Logo" />
|
||||||
|
</CHeaderBrand>
|
||||||
|
|
||||||
|
<CHeaderNav className="d-md-down-none mr-auto" />
|
||||||
|
|
||||||
|
<CHeaderNav className="px-3">
|
||||||
|
<LanguageSwitcher i18n={i18n} />
|
||||||
|
</CHeaderNav>
|
||||||
|
|
||||||
|
<CHeaderNav className="px-3">
|
||||||
|
<CPopover content={t('common.logout')}>
|
||||||
|
<CLink className="c-subheader-nav-link">
|
||||||
|
<CIcon
|
||||||
|
name="cilAccountLogout"
|
||||||
|
content={cilAccountLogout}
|
||||||
|
size="2xl"
|
||||||
|
onClick={() => logout(authToken, endpoints.ucentralsec)}
|
||||||
|
/>
|
||||||
|
</CLink>
|
||||||
|
</CPopover>
|
||||||
|
</CHeaderNav>
|
||||||
|
|
||||||
|
<CSubheader className="px-3 justify-content-between">
|
||||||
|
<CBreadcrumbRouter
|
||||||
|
className="border-0 c-subheader-nav m-0 px-0 px-md-3"
|
||||||
|
routes={translatedRoutes}
|
||||||
|
/>
|
||||||
|
</CSubheader>
|
||||||
|
</CHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
showSidebar: PropTypes.string.isRequired,
|
||||||
|
setShowSidebar: PropTypes.func.isRequired,
|
||||||
|
routes: PropTypes.arrayOf(Object).isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
i18n: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
logout: PropTypes.func.isRequired,
|
||||||
|
authToken: PropTypes.string.isRequired,
|
||||||
|
endpoints: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(Header);
|
||||||
48
src/layout/PageContainer/index.js
Normal file
48
src/layout/PageContainer/index.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
import React, { Suspense } from 'react';
|
||||||
|
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||||
|
import { v4 as createUuid } from 'uuid';
|
||||||
|
import { CContainer, CFade } from '@coreui/react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const loading = (
|
||||||
|
<div className="pt-3 text-center">
|
||||||
|
<div className="sk-spinner sk-spinner-pulse" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PageContainer = ({ t, routes, redirectTo }) => (
|
||||||
|
<main className="c-main">
|
||||||
|
<CContainer fluid>
|
||||||
|
<Suspense fallback={loading}>
|
||||||
|
<Switch>
|
||||||
|
{routes.map(
|
||||||
|
(route) =>
|
||||||
|
route.component && (
|
||||||
|
<Route
|
||||||
|
key={createUuid()}
|
||||||
|
path={route.path}
|
||||||
|
exact={route.exact}
|
||||||
|
name={t(route.name)}
|
||||||
|
render={(props) => (
|
||||||
|
<CFade>
|
||||||
|
<route.component {...props} />
|
||||||
|
</CFade>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<Redirect from="/" to={redirectTo} />
|
||||||
|
</Switch>
|
||||||
|
</Suspense>
|
||||||
|
</CContainer>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
|
||||||
|
PageContainer.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
routes: PropTypes.arrayOf(Object).isRequired,
|
||||||
|
redirectTo: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(PageContainer);
|
||||||
53
src/layout/Sidebar/index.js
Normal file
53
src/layout/Sidebar/index.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
CCreateElement,
|
||||||
|
CSidebar,
|
||||||
|
CSidebarBrand,
|
||||||
|
CSidebarNav,
|
||||||
|
CSidebarNavDivider,
|
||||||
|
CSidebarNavTitle,
|
||||||
|
CSidebarMinimizer,
|
||||||
|
CSidebarNavDropdown,
|
||||||
|
CSidebarNavItem,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const Sidebar = ({ showSidebar, setShowSidebar, logo, options, redirectTo }) => (
|
||||||
|
<CSidebar show={showSidebar} onShowChange={(val) => setShowSidebar(val)}>
|
||||||
|
<CSidebarBrand className="d-md-down-none" to={redirectTo}>
|
||||||
|
<img
|
||||||
|
className={[styles.sidebarImgFull, 'c-sidebar-brand-full'].join(' ')}
|
||||||
|
src={logo}
|
||||||
|
alt="OpenWifi"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
className={[styles.sidebarImgMinimized, 'c-sidebar-brand-minimized'].join(' ')}
|
||||||
|
src={logo}
|
||||||
|
alt="OpenWifi"
|
||||||
|
/>
|
||||||
|
</CSidebarBrand>
|
||||||
|
<CSidebarNav>
|
||||||
|
<CCreateElement
|
||||||
|
items={options}
|
||||||
|
components={{
|
||||||
|
CSidebarNavDivider,
|
||||||
|
CSidebarNavDropdown,
|
||||||
|
CSidebarNavItem,
|
||||||
|
CSidebarNavTitle,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CSidebarNav>
|
||||||
|
<CSidebarMinimizer className="c-d-md-down-none" />
|
||||||
|
</CSidebar>
|
||||||
|
);
|
||||||
|
|
||||||
|
Sidebar.propTypes = {
|
||||||
|
showSidebar: PropTypes.string.isRequired,
|
||||||
|
setShowSidebar: PropTypes.func.isRequired,
|
||||||
|
logo: PropTypes.string.isRequired,
|
||||||
|
options: PropTypes.arrayOf(Object).isRequired,
|
||||||
|
redirectTo: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(Sidebar);
|
||||||
9
src/layout/Sidebar/index.module.scss
Normal file
9
src/layout/Sidebar/index.module.scss
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.sidebarImgFull {
|
||||||
|
height: 100px;
|
||||||
|
width: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarImgMinimized {
|
||||||
|
height: 75px;
|
||||||
|
width: 75px;
|
||||||
|
}
|
||||||
59
webpack.config.js
Normal file
59
webpack.config.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
|
const path = require('path');
|
||||||
|
const nodeExternals = require('webpack-node-externals');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'production',
|
||||||
|
entry: './src/index.js',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
publicPath: '/dist/',
|
||||||
|
filename: 'index.js',
|
||||||
|
library: 'ucentral-libs',
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
umdNamedDefine: true,
|
||||||
|
},
|
||||||
|
externals: [nodeExternals()],
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|jsx)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: ['babel-loader', 'eslint-loader'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(css|scss)$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'style-loader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
modules: {
|
||||||
|
localIdentName: '[name]__[local]___[hash:base64:5]',
|
||||||
|
},
|
||||||
|
sourceMap: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
modules: [path.resolve('./node_modules'), path.resolve('./src')],
|
||||||
|
alias: {
|
||||||
|
src: path.resolve(__dirname, './src'),
|
||||||
|
react: path.resolve(__dirname, './node_modules/react'),
|
||||||
|
'react-dom': path.resolve(__dirname, './node_modules/react-dom'),
|
||||||
|
'prop-types': path.resolve(__dirname, './node_modules/prop-types'),
|
||||||
|
'react-router-dom': path.resolve(__dirname, './node_modules/react-router-dom'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [new CleanWebpackPlugin()],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user