[add] Jhipster base

This commit is contained in:
2018-08-06 21:49:34 -06:00
parent 1390427ec0
commit 11626e6efb
247 changed files with 145182 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Page Not Found</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="favicon.ico" />
<style>
* {
line-height: 1.2;
margin: 0;
}
html {
color: #888;
display: table;
font-family: sans-serif;
height: 100%;
text-align: center;
width: 100%;
}
body {
display: table-cell;
vertical-align: middle;
margin: 2em auto;
}
h1 {
color: #555;
font-size: 2em;
font-weight: 400;
}
p {
margin: 0 auto;
width: 280px;
}
@media only screen and (max-width: 280px) {
body, p {
width: 95%;
}
h1 {
font-size: 1.5em;
margin: 0 0 0.3em;
}
}
</style>
</head>
<body>
<h1>Page Not Found</h1>
<p>Sorry, but the page you were trying to view does not exist.</p>
</body>
</html>
<!-- IE needs 512+ bytes: http://blogs.msdn.com/b/ieinternals/archive/2010/08/19/http-error-pages-in-internet-explorer.aspx -->

View File

@@ -0,0 +1,28 @@
/*
* Bootstrap overrides https://v4-alpha.getbootstrap.com/getting-started/options/
* All values defined in bootstrap source
* https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss can be overwritten here
* Make sure not to add !default to values here
*/
// Options:
// Quickly modify global styling by enabling or disabling optional features.
$enable-rounded: true;
$enable-shadows: false;
$enable-gradients: false;
$enable-transitions: true;
$enable-hover-media-query: false;
$enable-grid-classes: true;
$enable-print-styles: true;
// Components:
// Define common padding and border radius sizes and more.
$border-radius: 0.15rem;
$border-radius-lg: 0.125rem;
$border-radius-sm: 0.1rem;
// Body:
// Settings for the `<body>` element.
$body-bg: #e4e5e6;

View File

@@ -0,0 +1,344 @@
// Override Boostrap variables
@import 'bootstrap-variables';
// Import Bootstrap source files from node_modules
@import 'node_modules/bootstrap/scss/bootstrap';
body {
background: #fafafa;
margin: 0;
}
a {
color: #533f03;
font-weight: bold;
}
a:hover {
color: #533f03;
font-weight: bold;
}
* {
-moz-box-sizing: border-box;
box-sizing: border-box;
&:after,
&::before {
-moz-box-sizing: border-box;
box-sizing: border-box;
}
}
.app-container {
box-sizing: border-box;
.view-container {
width: 100%;
height: calc(100% - 40px);
overflow-y: auto;
overflow-x: hidden;
padding: 1rem;
.card {
padding: 1rem;
}
.view-routes {
height: 100%;
> div {
height: 100%;
}
}
}
}
.fullscreen {
position: fixed;
top: 100px;
left: 0px;
width: 99% !important;
height: calc(100vh - 110px) !important;
margin: 5px;
z-index: 1000;
padding: 5px 25px 50px 25px !important;
}
/* ==========================================================================
Browser Upgrade Prompt
========================================================================== */
.browserupgrade {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
/* ==========================================================================
Custom button styles
========================================================================== */
.icon-button > .btn {
background-color: transparent;
border-color: transparent;
padding: 0.5rem;
line-height: 1rem;
&:hover {
background-color: transparent;
border-color: transparent;
}
&:focus {
-webkit-box-shadow: none;
box-shadow: none;
}
}
/* ==========================================================================
Generic styles
========================================================================== */
/* Temperory workaround for availity-reactstrap-validation */
.invalid-feedback {
display: inline;
}
/* other generic styles */
.title {
font-size: 1.25em;
margin: 1px 10px 1px 10px;
}
.description {
font-size: 0.9em;
margin: 1px 10px 1px 10px;
}
.shadow {
box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 6px, rgba(0, 0, 0, 0.12) 0px 1px 4px;
border-radius: 2px;
}
.error {
color: white;
background-color: red;
}
.break {
white-space: normal;
word-break: break-all;
}
.break-word {
white-space: normal;
word-break: keep-all;
}
.preserve-space {
white-space: pre-wrap;
}
/* padding helpers */
@mixin pad($size, $side) {
@if $size== '' {
@if $side== '' {
.pad {
padding: 10px !important;
}
} @else {
.pad {
padding-#{$side}: 10px !important;
}
}
} @else {
@if $side== '' {
.pad-#{$size} {
padding: #{$size}px !important;
}
} @else {
.pad-#{$side}-#{$size} {
padding-#{$side}: #{$size}px !important;
}
}
}
}
@include pad('', '');
@include pad('2', '');
@include pad('3', '');
@include pad('5', '');
@include pad('10', '');
@include pad('20', '');
@include pad('25', '');
@include pad('30', '');
@include pad('50', '');
@include pad('75', '');
@include pad('100', '');
@include pad('4', 'top');
@include pad('5', 'top');
@include pad('10', 'top');
@include pad('20', 'top');
@include pad('25', 'top');
@include pad('30', 'top');
@include pad('50', 'top');
@include pad('75', 'top');
@include pad('100', 'top');
@include pad('4', 'bottom');
@include pad('5', 'bottom');
@include pad('10', 'bottom');
@include pad('20', 'bottom');
@include pad('25', 'bottom');
@include pad('30', 'bottom');
@include pad('50', 'bottom');
@include pad('75', 'bottom');
@include pad('100', 'bottom');
@include pad('5', 'right');
@include pad('10', 'right');
@include pad('20', 'right');
@include pad('25', 'right');
@include pad('30', 'right');
@include pad('50', 'right');
@include pad('75', 'right');
@include pad('100', 'right');
@include pad('5', 'left');
@include pad('10', 'left');
@include pad('20', 'left');
@include pad('25', 'left');
@include pad('30', 'left');
@include pad('50', 'left');
@include pad('75', 'left');
@include pad('100', 'left');
@mixin no-padding($side) {
@if $side== 'all' {
.no-padding {
padding: 0 !important;
}
} @else {
.no-padding-#{$side} {
padding-#{$side}: 0 !important;
}
}
}
@include no-padding('left');
@include no-padding('right');
@include no-padding('top');
@include no-padding('bottom');
@include no-padding('all');
/* end of padding helpers */
.no-margin {
margin: 0px;
}
@mixin voffset($size) {
@if $size== '' {
.voffset {
margin-top: 2px !important;
}
} @else {
.voffset-#{$size} {
margin-top: #{$size}px !important;
}
}
}
@include voffset('');
@include voffset('5');
@include voffset('10');
@include voffset('15');
@include voffset('30');
@include voffset('40');
@include voffset('60');
@include voffset('80');
@include voffset('100');
@include voffset('150');
.readonly {
background-color: #eee;
opacity: 1;
}
/* ==========================================================================
make sure browsers use the pointer cursor for anchors, even with no href
========================================================================== */
a:hover {
cursor: pointer;
}
.hand {
cursor: pointer;
}
button.anchor-btn {
background: none;
border: none;
padding: 0;
align-items: initial;
text-align: initial;
width: 100%;
}
a.anchor-btn:hover {
text-decoration: none;
}
/* ==========================================================================
Metrics and Health styles
========================================================================== */
#threadDump .popover,
#healthCheck .popover {
top: inherit;
display: block;
font-size: 10px;
max-width: 1024px;
}
.thread-dump-modal-lock {
max-width: 450px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#healthCheck .popover {
margin-left: -50px;
}
.health-details {
min-width: 400px;
}
/* ==========================================================================
start Password strength bar style
========================================================================== */
ul#strengthBar {
display: inline;
list-style: none;
margin: 0;
margin-left: 15px;
padding: 0;
vertical-align: 2px;
}
.point:last {
margin: 0 !important;
}
.point {
background: #ddd;
border-radius: 2px;
display: inline-block;
height: 5px;
margin-right: 1px;
width: 20px;
}
/* bootstrap 3 input-group 100% width
http://stackoverflow.com/questions/23436430/bootstrap-3-input-group-100-width */
.width-min {
width: 1% !important;
}
/* jhipster-needle-css-add-main JHipster will add new css style */

View File

@@ -0,0 +1,73 @@
import 'react-toastify/dist/ReactToastify.css';
import './app.scss';
import React from 'react';
import { connect } from 'react-redux';
import { Card } from 'reactstrap';
import { HashRouter as Router } from 'react-router-dom';
import { ToastContainer, toast } from 'react-toastify';
import { IRootState } from 'app/shared/reducers';
import { getSession } from 'app/shared/reducers/authentication';
import { getProfile } from 'app/shared/reducers/application-profile';
import Header from 'app/shared/layout/header/header';
import Footer from 'app/shared/layout/footer/footer';
import { hasAnyAuthority } from 'app/shared/auth/private-route';
import ErrorBoundary from 'app/shared/error/error-boundary';
import { AUTHORITIES } from 'app/config/constants';
import AppRoutes from 'app/routes';
export interface IAppProps extends StateProps, DispatchProps {}
export class App extends React.Component<IAppProps> {
componentDidMount() {
this.props.getSession();
this.props.getProfile();
}
render() {
const paddingTop = '60px';
return (
<Router>
<div className="app-container" style={{ paddingTop }}>
<ToastContainer position={toast.POSITION.TOP_LEFT} className="toastify-container" toastClassName="toastify-toast" />
<ErrorBoundary>
<Header
isAuthenticated={this.props.isAuthenticated}
isAdmin={this.props.isAdmin}
ribbonEnv={this.props.ribbonEnv}
isInProduction={this.props.isInProduction}
isSwaggerEnabled={this.props.isSwaggerEnabled}
/>
</ErrorBoundary>
<div className="container-fluid view-container" id="app-view-container">
<Card className="jh-card">
<ErrorBoundary>
<AppRoutes />
</ErrorBoundary>
</Card>
<Footer />
</div>
</div>
</Router>
);
}
}
const mapStateToProps = ({ authentication, applicationProfile }: IRootState) => ({
isAuthenticated: authentication.isAuthenticated,
isAdmin: hasAnyAuthority(authentication.account.authorities, [AUTHORITIES.ADMIN]),
ribbonEnv: applicationProfile.ribbonEnv,
isInProduction: applicationProfile.inProduction,
isSwaggerEnabled: applicationProfile.isSwaggerEnabled
});
const mapDispatchToProps = { getSession, getProfile };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);

View File

@@ -0,0 +1,25 @@
import axios from 'axios';
import { getBasePath, Storage } from 'react-jhipster';
import { SERVER_API_URL } from 'app/config/constants';
const TIMEOUT = 1000000; // 10000
const setupAxiosInterceptors = onUnauthenticated => {
const onRequestSuccess = config => {
config.timeout = TIMEOUT;
config.url = `${SERVER_API_URL}${config.url}`;
return config;
};
const onResponseSuccess = response => response;
const onResponseError = err => {
const status = err.status || err.response.status;
if (status === 403 || status === 401) {
onUnauthenticated();
}
return Promise.reject(err);
};
axios.interceptors.request.use(onRequestSuccess);
axios.interceptors.response.use(onResponseSuccess, onResponseError);
};
export default setupAxiosInterceptors;

View File

@@ -0,0 +1,23 @@
const config = {
VERSION: process.env.VERSION
};
export default config;
export const SERVER_API_URL = process.env.SERVER_API_URL;
export const AUTHORITIES = {
ADMIN: 'ROLE_ADMIN',
USER: 'ROLE_USER'
};
export const messages = {
DATA_ERROR_ALERT: 'Internal Error'
};
export const APP_DATE_FORMAT = 'DD/MM/YY HH:mm';
export const APP_TIMESTAMP_FORMAT = 'DD/MM/YY HH:mm:ss';
export const APP_LOCAL_DATE_FORMAT = 'DD/MM/YYYY';
export const APP_LOCAL_DATETIME_FORMAT = 'YYYY-MM-DDThh:mm';
export const APP_WHOLE_NUMBER_FORMAT = '0,0';
export const APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT = '0,0.[00]';

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { createDevTools } from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
// You can toggle visibility of devTools with ctrl + H
// and change their position with ctrl + Q
export default createDevTools(
<DockMonitor toggleVisibilityKey="ctrl-h" changePositionKey="ctrl-q" defaultIsVisible={false}>
<LogMonitor />
</DockMonitor>
);

View File

@@ -0,0 +1,38 @@
import { isPromise } from 'react-jhipster';
const getErrorMessage = errorData => {
let message = errorData.message;
if (errorData.fieldErrors) {
errorData.fieldErrors.forEach(fErr => {
message += `\nfield: ${fErr.field}, Object: ${fErr.objectName}, message: ${fErr.message}\n`;
});
}
return message;
};
export default () => next => action => {
// If not a promise, continue on
if (!isPromise(action.payload)) {
return next(action);
}
/**
*
* The error middleware serves to dispatch the initial pending promise to
* the promise middleware, but adds a `catch`.
* It need not run in production
*/
if (process.env.NODE_ENV === 'development') {
// Dispatch initial pending promise, but catch any errors
return next(action).catch(error => {
console.error(`${action.type} caught at middleware with reason: ${JSON.stringify(error.message)}.`);
if (error && error.response && error.response.data) {
const message = getErrorMessage(error.response.data);
console.error(`Actual cause: ${message}`);
}
return Promise.reject(error);
});
}
return next(action);
};

View File

@@ -0,0 +1,63 @@
import { faSort } from '@fortawesome/free-solid-svg-icons/faSort';
import { faEye } from '@fortawesome/free-solid-svg-icons/faEye';
import { faSync } from '@fortawesome/free-solid-svg-icons/faSync';
import { faBan } from '@fortawesome/free-solid-svg-icons/faBan';
import { faTrash } from '@fortawesome/free-solid-svg-icons/faTrash';
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons/faArrowLeft';
import { faSave } from '@fortawesome/free-solid-svg-icons/faSave';
import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt';
import { faUser } from '@fortawesome/free-solid-svg-icons/faUser';
import { faHdd } from '@fortawesome/free-solid-svg-icons/faHdd';
import { faTachometerAlt } from '@fortawesome/free-solid-svg-icons/faTachometerAlt';
import { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart';
import { faList } from '@fortawesome/free-solid-svg-icons/faList';
import { faTasks } from '@fortawesome/free-solid-svg-icons/faTasks';
import { faBook } from '@fortawesome/free-solid-svg-icons/faBook';
import { faClock } from '@fortawesome/free-solid-svg-icons/faClock';
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons/faSignInAlt';
import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons/faSignOutAlt';
import { faThList } from '@fortawesome/free-solid-svg-icons/faThList';
import { faUserPlus } from '@fortawesome/free-solid-svg-icons/faUserPlus';
import { faWrench } from '@fortawesome/free-solid-svg-icons/faWrench';
import { faAsterisk } from '@fortawesome/free-solid-svg-icons/faAsterisk';
import { faFlag } from '@fortawesome/free-solid-svg-icons/faFlag';
import { faBell } from '@fortawesome/free-solid-svg-icons/faBell';
import { faHome } from '@fortawesome/free-solid-svg-icons/faHome';
import { faTimesCircle } from '@fortawesome/free-solid-svg-icons/faTimesCircle';
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
import { faRoad } from '@fortawesome/free-solid-svg-icons/faRoad';
import { library } from '@fortawesome/fontawesome-svg-core';
export const loadIcons = () => {
library.add(
faSort,
faEye,
faSync,
faBan,
faTrash,
faArrowLeft,
faSave,
faPlus,
faPencilAlt,
faUser,
faTachometerAlt,
faHeart,
faList,
faTasks,
faBook,
faHdd,
faClock,
faSignInAlt,
faSignOutAlt,
faWrench,
faThList,
faUserPlus,
faAsterisk,
faFlag,
faBell,
faHome,
faRoad
);
};

View File

@@ -0,0 +1,14 @@
export default () => next => action => {
if (process.env.NODE_ENV !== 'production') {
const { type, payload, meta } = action;
console.groupCollapsed(type);
// tslint:disable-next-line
console.log('Payload:', payload);
// tslint:disable-next-line
console.log('Meta:', meta);
console.groupEnd();
}
return next(action);
};

View File

@@ -0,0 +1,99 @@
import { isPromise } from 'react-jhipster';
import { toast } from 'react-toastify';
const addErrorAlert = (message, key?, data?) => {
toast.error(message);
};
export default () => next => action => {
// If not a promise, continue on
if (!isPromise(action.payload)) {
return next(action);
}
/**
*
* The notification middleware serves to dispatch the initial pending promise to
* the promise middleware, but adds a `then` and `catch.
*/
return next(action)
.then(response => {
if (action.meta && action.meta.successMessage) {
toast.success(action.meta.successMessage);
} else if (response && response.action && response.action.payload && response.action.payload.headers) {
const headers = response.action.payload.headers;
let alert: string = null;
Object.entries(headers).forEach(([k, v]: [string, string]) => {
if (k.endsWith('app-alert')) {
alert = v;
}
});
if (alert) {
toast.success(alert);
}
}
return Promise.resolve(response);
})
.catch(error => {
if (action.meta && action.meta.errorMessage) {
toast.error(action.meta.errorMessage);
} else if (error && error.response) {
const response = error.response;
const data = response.data;
if (!(response.status === 401 && (error.message === '' || (data && data.path && data.path.includes('/api/account'))))) {
let i;
switch (response.status) {
// connection refused, server not reachable
case 0:
addErrorAlert('Server not reachable', 'error.server.not.reachable');
break;
case 400:
const headers = Object.entries(response.headers);
let errorHeader = null;
let entityKey = null;
headers.forEach(([k, v]: [string, string]) => {
if (k.endsWith('app-error')) {
errorHeader = v;
} else if (k.endsWith('app-params')) {
entityKey = v;
}
});
if (errorHeader) {
const entityName = entityKey;
addErrorAlert(errorHeader, errorHeader, { entityName });
} else if (data !== '' && data.fieldErrors) {
const fieldErrors = data.fieldErrors;
for (i = 0; i < fieldErrors.length; i++) {
const fieldError = fieldErrors[i];
// convert 'something[14].other[4].id' to 'something[].other[].id' so translations can be written to it
const convertedField = fieldError.field.replace(/\[\d*\]/g, '[]');
const fieldName = convertedField.charAt(0).toUpperCase() + convertedField.slice(1);
addErrorAlert(`Error on field "${fieldName}"`, `error.${fieldError.message}`, { fieldName });
}
} else if (data !== '' && data.message) {
addErrorAlert(data.message, data.message, data.params);
} else {
addErrorAlert(data);
}
break;
case 404:
addErrorAlert('Not found', 'error.url.not.found');
break;
default:
if (data !== '' && data.message) {
addErrorAlert(data.message);
} else {
addErrorAlert(data);
}
}
}
} else if (error && error.message) {
toast.error(error.message);
} else {
toast.error('Unknown error!');
}
return Promise.reject(error);
});
};

View File

@@ -0,0 +1,29 @@
import { createStore, applyMiddleware, compose } from 'redux';
import promiseMiddleware from 'redux-promise-middleware';
import thunkMiddleware from 'redux-thunk';
import reducer, { IRootState } from 'app/shared/reducers';
import DevTools from './devtools';
import errorMiddleware from './error-middleware';
import notificationMiddleware from './notification-middleware';
import loggerMiddleware from './logger-middleware';
import { loadingBarMiddleware } from 'react-redux-loading-bar';
const defaultMiddlewares = [
thunkMiddleware,
errorMiddleware,
notificationMiddleware,
promiseMiddleware(),
loadingBarMiddleware(),
loggerMiddleware
];
const composedMiddlewares = middlewares =>
process.env.NODE_ENV === 'development'
? compose(
applyMiddleware(...defaultMiddlewares, ...middlewares),
DevTools.instrument()
)
: compose(applyMiddleware(...defaultMiddlewares, ...middlewares));
const initialize = (initialState?: IRootState, middlewares = []) => createStore(reducer, initialState, composedMiddlewares(middlewares));
export default initialize;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { Switch } from 'react-router-dom';
// tslint:disable-next-line:no-unused-variable
import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route';
/* jhipster-needle-add-route-import - JHipster will add routes here */
const Routes = ({ match }) => (
<div>
<Switch>
{/* prettier-ignore */}
{/* jhipster-needle-add-route-path - JHipster will routes here */}
</Switch>
</div>
);
export default Routes;

View File

@@ -0,0 +1,50 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { bindActionCreators } from 'redux';
import { AppContainer } from 'react-hot-loader';
import DevTools from './config/devtools';
import initStore from './config/store';
import setupAxiosInterceptors from './config/axios-interceptor';
import { clearAuthentication } from './shared/reducers/authentication';
import ErrorBoundary from './shared/error/error-boundary';
import AppComponent from './app';
import { loadIcons } from './config/icon-loader';
const devTools = process.env.NODE_ENV === 'development' ? <DevTools /> : null;
const store = initStore();
const actions = bindActionCreators({ clearAuthentication }, store.dispatch);
setupAxiosInterceptors(() => actions.clearAuthentication('login.error.unauthorized'));
loadIcons();
const rootEl = document.getElementById('root');
const render = Component =>
ReactDOM.render(
<ErrorBoundary>
<AppContainer>
<Provider store={store}>
<div>
{/* If this slows down the app in dev disable it and enable when required */}
{devTools}
<Component />
</div>
</Provider>
</AppContainer>
</ErrorBoundary>,
rootEl
);
render(AppComponent);
// This is quite unstable
// if (module.hot) {
// module.hot.accept('./app', () => {
// const NextApp = require<{ default: typeof AppComponent }>('./app').default;
// render(NextApp);
// });
// }

View File

@@ -0,0 +1,51 @@
import axios from 'axios';
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
export const ACTION_TYPES = {
ACTIVATE_ACCOUNT: 'activate/ACTIVATE_ACCOUNT',
RESET: 'activate/RESET'
};
const initialState = {
activationSuccess: false,
activationFailure: false
};
export type ActivateState = Readonly<typeof initialState>;
// Reducer
export default (state: ActivateState = initialState, action): ActivateState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.ACTIVATE_ACCOUNT):
return {
...state
};
case FAILURE(ACTION_TYPES.ACTIVATE_ACCOUNT):
return {
...state,
activationFailure: true
};
case SUCCESS(ACTION_TYPES.ACTIVATE_ACCOUNT):
return {
...state,
activationSuccess: true
};
case ACTION_TYPES.RESET:
return {
...initialState
};
default:
return state;
}
};
// Actions
export const activateAction = key => ({
type: ACTION_TYPES.ACTIVATE_ACCOUNT,
payload: axios.get('api/activate?key=' + key)
});
export const reset = () => ({
type: ACTION_TYPES.RESET
});

View File

@@ -0,0 +1,66 @@
import React from 'react';
import { connect } from 'react-redux';
import { Link, RouteComponentProps } from 'react-router-dom';
import { Row, Col, Alert } from 'reactstrap';
import { IRootState } from 'app/shared/reducers';
import { activateAction, reset } from './activate.reducer';
const successAlert = (
<Alert color="success">
<strong>Your user account has been activated.</strong> Please
<Link to="/login" className="alert-link">
sign in
</Link>.
</Alert>
);
const failureAlert = (
<Alert color="danger">
<strong>Your user could not be activated.</strong> Please use the registration form to sign up.
</Alert>
);
export interface IActivateProps extends StateProps, DispatchProps, RouteComponentProps<{ key: any }> {}
export class ActivatePage extends React.Component<IActivateProps> {
componentWillUnmount() {
this.props.reset();
}
componentDidMount() {
const { key } = this.props.match.params;
this.props.activateAction(key);
}
render() {
const { activationSuccess, activationFailure } = this.props;
return (
<div>
<Row className="justify-content-center">
<Col md="8">
<h1>Activation</h1>
{activationSuccess ? successAlert : undefined}
{activationFailure ? failureAlert : undefined}
</Col>
</Row>
</div>
);
}
}
const mapStateToProps = ({ activate }: IRootState) => ({
activationSuccess: activate.activationSuccess,
activationFailure: activate.activationFailure
});
const mapDispatchToProps = { activateAction, reset };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(ActivatePage);

View File

@@ -0,0 +1,17 @@
import React from 'react';
import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route';
import Settings from './settings/settings';
import Password from './password/password';
import Sessions from './sessions/sessions';
const Routes = ({ match }) => (
<div>
<ErrorBoundaryRoute path={`${match.url}/settings`} component={Settings} />
<ErrorBoundaryRoute path={`${match.url}/password`} component={Password} />
<ErrorBoundaryRoute path={`${match.url}/sessions`} component={Sessions} />
</div>
);
export default Routes;

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { connect } from 'react-redux';
import { Alert, Col, Row, Button } from 'reactstrap';
import { AvForm, AvField } from 'availity-reactstrap-validation';
import { RouteComponentProps } from 'react-router-dom';
import { IRootState } from 'app/shared/reducers';
import { handlePasswordResetFinish, reset } from '../password-reset.reducer';
import PasswordStrengthBar from 'app/shared/layout/password/password-strength-bar';
export interface IPasswordResetFinishProps extends DispatchProps, RouteComponentProps<{ key: string }> {}
export interface IPasswordResetFinishState {
password: string;
key: string;
}
export class PasswordResetFinishPage extends React.Component<IPasswordResetFinishProps, IPasswordResetFinishState> {
state: IPasswordResetFinishState = {
password: '',
key: this.props.match.params.key
};
componentWillUnmount() {
this.props.reset();
}
handleValidSubmit = (event, values) => {
this.props.handlePasswordResetFinish(this.state.key, values.newPassword);
};
updatePassword = event => {
this.setState({ password: event.target.value });
};
getResetForm() {
return (
<AvForm onValidSubmit={this.handleValidSubmit}>
<AvField
name="newPassword"
label="New password"
placeholder={'New password'}
type="password"
validate={{
required: { value: true, errorMessage: 'Your password is required.' },
minLength: { value: 4, errorMessage: 'Your password is required to be at least 4 characters.' },
maxLength: { value: 50, errorMessage: 'Your password cannot be longer than 50 characters.' }
}}
onChange={this.updatePassword}
/>
<PasswordStrengthBar password={this.state.password} />
<AvField
name="confirmPassword"
label="New password confirmation"
placeholder="Confirm the new password"
type="password"
validate={{
required: { value: true, errorMessage: 'Your confirmation password is required.' },
minLength: { value: 4, errorMessage: 'Your confirmation password is required to be at least 4 characters.' },
maxLength: { value: 50, errorMessage: 'Your confirmation password cannot be longer than 50 characters.' },
match: { value: 'newPassword', errorMessage: 'The password and its confirmation do not match!' }
}}
/>
<Button color="success" type="submit">
Validate new password
</Button>
</AvForm>
);
}
render() {
const { key } = this.state;
return (
<div>
<Row className="justify-content-center">
<Col md="4">
<h1>Reset password</h1>
<div>{key ? this.getResetForm() : null}</div>
</Col>
</Row>
</div>
);
}
}
const mapDispatchToProps = { handlePasswordResetFinish, reset };
type DispatchProps = typeof mapDispatchToProps;
export default connect(
null,
mapDispatchToProps
)(PasswordResetFinishPage);

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { connect } from 'react-redux';
import { AvForm, AvField } from 'availity-reactstrap-validation';
import { Button, Alert, Col, Row } from 'reactstrap';
import { IRootState } from 'app/shared/reducers';
import { handlePasswordResetInit, reset } from '../password-reset.reducer';
export type IPasswordResetInitProps = DispatchProps;
export class PasswordResetInit extends React.Component<IPasswordResetInitProps> {
componentWillUnmount() {
this.props.reset();
}
handleValidSubmit = (event, values) => {
this.props.handlePasswordResetInit(values.email);
event.preventDefault();
};
render() {
return (
<div>
<Row className="justify-content-center">
<Col md="8">
<h1>Reset your password</h1>
<Alert color="warning">
<p>Enter the email address you used to register</p>
</Alert>
<AvForm onValidSubmit={this.handleValidSubmit}>
<AvField
name="email"
label="Email"
placeholder="Your email"
type="email"
validate={{
required: { value: true, errorMessage: 'Your email is required.' },
minLength: { value: 5, errorMessage: 'Your email is required to be at least 5 characters.' },
maxLength: { value: 254, errorMessage: 'Your email cannot be longer than 50 characters.' }
}}
/>
<Button color="primary" type="submit">
Reset password
</Button>
</AvForm>
</Col>
</Row>
</div>
);
}
}
const mapDispatchToProps = { handlePasswordResetInit, reset };
type DispatchProps = typeof mapDispatchToProps;
export default connect(
null,
mapDispatchToProps
)(PasswordResetInit);

View File

@@ -0,0 +1,74 @@
import axios from 'axios';
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
export const ACTION_TYPES = {
RESET_PASSWORD_INIT: 'passwordReset/RESET_PASSWORD_INIT',
RESET_PASSWORD_FINISH: 'passwordReset/RESET_PASSWORD_FINISH',
RESET: 'passwordReset/RESET'
};
const initialState = {
loading: false,
resetPasswordSuccess: false,
resetPasswordFailure: false
};
export type PasswordResetState = Readonly<typeof initialState>;
// Reducer
export default (state: PasswordResetState = initialState, action): PasswordResetState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.RESET_PASSWORD_FINISH):
case REQUEST(ACTION_TYPES.RESET_PASSWORD_INIT):
return {
...state,
loading: true
};
case FAILURE(ACTION_TYPES.RESET_PASSWORD_FINISH):
case FAILURE(ACTION_TYPES.RESET_PASSWORD_INIT):
return {
...initialState,
loading: false,
resetPasswordFailure: true
};
case SUCCESS(ACTION_TYPES.RESET_PASSWORD_FINISH):
case SUCCESS(ACTION_TYPES.RESET_PASSWORD_INIT):
return {
...initialState,
loading: false,
resetPasswordSuccess: true
};
case ACTION_TYPES.RESET:
return {
...initialState
};
default:
return state;
}
};
const apiUrl = 'api/account/reset-password';
// Actions
export const handlePasswordResetInit = mail => ({
type: ACTION_TYPES.RESET_PASSWORD_INIT,
// If the content-type isn't set that way, axios will try to encode the body and thus modify the data sent to the server.
payload: axios.post(`${apiUrl}/init`, mail, { headers: { ['Content-Type']: 'text/plain' } }),
meta: {
successMessage: 'Check your emails for details on how to reset your password.',
errorMessage: "<strong>Email address isn't registered!</strong> Please check and try again"
}
});
export const handlePasswordResetFinish = (key, newPassword) => ({
type: ACTION_TYPES.RESET_PASSWORD_FINISH,
payload: axios.post(`${apiUrl}/finish`, { key, newPassword }),
meta: {
successMessage: '<strong>Your password has been reset.</strong> Please '
}
});
export const reset = () => ({
type: ACTION_TYPES.RESET
});

View File

@@ -0,0 +1,66 @@
import axios from 'axios';
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
export const ACTION_TYPES = {
UPDATE_PASSWORD: 'account/UPDATE_PASSWORD',
RESET: 'account/RESET'
};
const initialState = {
loading: false,
errorMessage: null,
updateSuccess: false,
updateFailure: false
};
export type PasswordState = Readonly<typeof initialState>;
// Reducer
export default (state: PasswordState = initialState, action): PasswordState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.UPDATE_PASSWORD):
return {
...initialState,
errorMessage: null,
updateSuccess: false,
loading: true
};
case FAILURE(ACTION_TYPES.UPDATE_PASSWORD):
return {
...initialState,
loading: false,
updateSuccess: false,
updateFailure: true
};
case SUCCESS(ACTION_TYPES.UPDATE_PASSWORD):
return {
...initialState,
loading: false,
updateSuccess: true,
updateFailure: false
};
case ACTION_TYPES.RESET:
return {
...initialState
};
default:
return state;
}
};
// Actions
const apiUrl = 'api/account';
export const savePassword = (currentPassword, newPassword) => ({
type: ACTION_TYPES.UPDATE_PASSWORD,
payload: axios.post(`${apiUrl}/change-password`, { currentPassword, newPassword }),
meta: {
successMessage: '<strong>Password changed!</strong>',
errorMessage: '<strong>An error has occurred!</strong> The password could not be changed.'
}
});
export const reset = () => ({
type: ACTION_TYPES.RESET
});

View File

@@ -0,0 +1,119 @@
import React from 'react';
import { connect } from 'react-redux';
import { AvForm, AvField } from 'availity-reactstrap-validation';
import { Row, Col, Button } from 'reactstrap';
import { IRootState } from 'app/shared/reducers';
import { getSession } from 'app/shared/reducers/authentication';
import PasswordStrengthBar from 'app/shared/layout/password/password-strength-bar';
import { savePassword, reset } from './password.reducer';
export interface IUserPasswordProps extends StateProps, DispatchProps {}
export interface IUserPasswordState {
password: string;
}
export class PasswordPage extends React.Component<IUserPasswordProps, IUserPasswordState> {
state: IUserPasswordState = {
password: ''
};
componentDidMount() {
this.props.reset();
this.props.getSession();
}
componentWillUnmount() {
this.props.reset();
}
handleValidSubmit = (event, values) => {
this.props.savePassword(values.currentPassword, values.newPassword);
};
updatePassword = event => {
this.setState({ password: event.target.value });
};
render() {
const { account } = this.props;
return (
<div>
<Row className="justify-content-center">
<Col md="8">
<h2 id="password-title">Password for {account.login}</h2>
<AvForm id="password-form" onValidSubmit={this.handleValidSubmit}>
<AvField
name="currentPassword"
label="Current password"
placeholder="Current password"
type="password"
validate={{
required: { value: true, errorMessage: 'Your password is required.' }
}}
/>
<AvField
name="newPassword"
label="New password"
placeholder="New password"
type="password"
validate={{
required: { value: true, errorMessage: 'Your password is required.' },
minLength: { value: 4, errorMessage: 'Your password is required to be at least 4 characters.' },
maxLength: { value: 50, errorMessage: 'Your password cannot be longer than 50 characters.' }
}}
onChange={this.updatePassword}
/>
<PasswordStrengthBar password={this.state.password} />
<AvField
name="confirmPassword"
label="New password confirmation"
placeholder="Confirm the new password"
type="password"
validate={{
required: {
value: true,
errorMessage: 'Your confirmation password is required.'
},
minLength: {
value: 4,
errorMessage: 'Your confirmation password is required to be at least 4 characters.'
},
maxLength: {
value: 50,
errorMessage: 'Your confirmation password cannot be longer than 50 characters.'
},
match: {
value: 'newPassword',
errorMessage: 'The password and its confirmation do not match!'
}
}}
/>
<Button color="success" type="submit">
Save
</Button>
</AvForm>
</Col>
</Row>
</div>
);
}
}
const mapStateToProps = ({ authentication }: IRootState) => ({
account: authentication.account,
isAuthenticated: authentication.isAuthenticated
});
const mapDispatchToProps = { getSession, savePassword, reset };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(PasswordPage);

View File

@@ -0,0 +1,58 @@
import axios from 'axios';
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
export const ACTION_TYPES = {
CREATE_ACCOUNT: 'register/CREATE_ACCOUNT',
RESET: 'register/RESET'
};
const initialState = {
loading: false,
registrationSuccess: false,
registrationFailure: false,
errorMessage: null
};
export type RegisterState = Readonly<typeof initialState>;
// Reducer
export default (state: RegisterState = initialState, action): RegisterState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.CREATE_ACCOUNT):
return {
...state,
loading: true
};
case FAILURE(ACTION_TYPES.CREATE_ACCOUNT):
return {
...initialState,
registrationFailure: true,
errorMessage: action.payload.response.data.errorKey
};
case SUCCESS(ACTION_TYPES.CREATE_ACCOUNT):
return {
...initialState,
registrationSuccess: true
};
case ACTION_TYPES.RESET:
return {
...initialState
};
default:
return state;
}
};
// Actions
export const handleRegister = (login, email, password, langKey = 'en') => ({
type: ACTION_TYPES.CREATE_ACCOUNT,
payload: axios.post('api/register', { login, email, password, langKey }),
meta: {
successMessage: '<strong>Registration saved!</strong> Please check your email for confirmation.'
}
});
export const reset = () => ({
type: ACTION_TYPES.RESET
});

View File

@@ -0,0 +1,120 @@
import React from 'react';
import { connect } from 'react-redux';
import { AvForm, AvField } from 'availity-reactstrap-validation';
import { Row, Col, Alert, Button } from 'reactstrap';
import PasswordStrengthBar from 'app/shared/layout/password/password-strength-bar';
import { IRootState } from 'app/shared/reducers';
import { handleRegister, reset } from './register.reducer';
export type IRegisterProps = DispatchProps;
export interface IRegisterState {
password: string;
}
export class RegisterPage extends React.Component<IRegisterProps, IRegisterState> {
state: IRegisterState = {
password: ''
};
componentWillUnmount() {
this.props.reset();
}
handleValidSubmit = (event, values) => {
this.props.handleRegister(values.username, values.email, values.firstPassword);
event.preventDefault();
};
updatePassword = event => {
this.setState({ password: event.target.value });
};
render() {
return (
<div>
<Row className="justify-content-center">
<Col md="8">
<h1 id="register-title">Registration</h1>
</Col>
</Row>
<Row className="justify-content-center">
<Col md="8">
<AvForm id="register-form" onValidSubmit={this.handleValidSubmit}>
<AvField
name="username"
label="Username"
placeholder="Your username"
validate={{
required: { value: true, errorMessage: 'Your username is required.' },
pattern: { value: '^[_.@A-Za-z0-9-]*$', errorMessage: 'Your username can only contain letters and digits.' },
minLength: { value: 1, errorMessage: 'Your username is required to be at least 1 character.' },
maxLength: { value: 50, errorMessage: 'Your username cannot be longer than 50 characters.' }
}}
/>
<AvField
name="email"
label="Email"
placeholder="Your email"
type="email"
validate={{
required: { value: true, errorMessage: 'Your email is required.' },
minLength: { value: 5, errorMessage: 'Your email is required to be at least 5 characters.' },
maxLength: { value: 254, errorMessage: 'Your email cannot be longer than 50 characters.' }
}}
/>
<AvField
name="firstPassword"
label="New password"
placeholder="New password"
type="password"
onChange={this.updatePassword}
validate={{
required: { value: true, errorMessage: 'Your password is required.' },
minLength: { value: 4, errorMessage: 'Your password is required to be at least 4 characters.' },
maxLength: { value: 50, errorMessage: 'Your password cannot be longer than 50 characters.' }
}}
/>
<PasswordStrengthBar password={this.state.password} />
<AvField
name="secondPassword"
label="New password confirmation"
placeholder="Confirm the new password"
type="password"
validate={{
required: { value: true, errorMessage: 'Your confirmation password is required.' },
minLength: { value: 4, errorMessage: 'Your confirmation password is required to be at least 4 characters.' },
maxLength: { value: 50, errorMessage: 'Your confirmation password cannot be longer than 50 characters.' },
match: { value: 'firstPassword', errorMessage: 'The password and its confirmation do not match!' }
}}
/>
<Button id="register-submit" color="primary" type="submit">
Register
</Button>
</AvForm>
<p>&nbsp;</p>
<Alert color="warning">
<span>If you want to</span>
<a className="alert-link"> sign in</a>
<span>
, you can try the default accounts:
<br />- Administrator (login="admin" and password="admin")
<br />- User (login="user" and password="user").
</span>
</Alert>
</Col>
</Row>
</div>
);
}
}
const mapDispatchToProps = { handleRegister, reset };
type DispatchProps = typeof mapDispatchToProps;
export default connect(
null,
mapDispatchToProps
)(RegisterPage);

View File

@@ -0,0 +1,67 @@
import axios from 'axios';
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
export const ACTION_TYPES = {
FIND_ALL: 'sessions/FIND_ALL',
INVALIDATE: 'sessions/INVALIDATE'
};
const initialState = {
loading: false,
sessions: [],
updateSuccess: false,
updateFailure: false
};
export type SessionsState = Readonly<typeof initialState>;
// Reducer
export default (state: SessionsState = initialState, action): SessionsState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.FIND_ALL):
case REQUEST(ACTION_TYPES.INVALIDATE):
return {
...state,
loading: true
};
case FAILURE(ACTION_TYPES.FIND_ALL):
return {
...state,
loading: false
};
case FAILURE(ACTION_TYPES.INVALIDATE):
return {
...state,
loading: false,
updateFailure: true
};
case SUCCESS(ACTION_TYPES.FIND_ALL):
return {
...state,
loading: false,
sessions: action.payload.data
};
case SUCCESS(ACTION_TYPES.INVALIDATE):
return {
...state,
loading: false,
updateSuccess: true
};
default:
return state;
}
};
// Actions
const apiURL = '/api/account/sessions/';
export const findAll = () => ({
type: ACTION_TYPES.FIND_ALL,
payload: axios.get(apiURL)
});
export const invalidateSession = series => ({
type: ACTION_TYPES.INVALIDATE,
payload: axios.delete(`${apiURL}${series}`)
});

View File

@@ -0,0 +1,99 @@
import React from 'react';
import { connect } from 'react-redux';
import { Alert, Table, Button } from 'reactstrap';
import { getSession } from 'app/shared/reducers/authentication';
import { IRootState } from 'app/shared/reducers';
import { findAll, invalidateSession } from './sessions.reducer';
export interface ISessionsProps extends StateProps, DispatchProps {}
export class SessionsPage extends React.Component<ISessionsProps> {
componentDidMount() {
this.props.getSession();
this.props.findAll();
}
doSessionInvalidation = series => () => {
this.props.invalidateSession(series);
this.props.findAll();
};
refreshList = () => {
this.props.findAll();
};
render() {
const { account, sessions, updateSuccess, updateFailure } = this.props;
return (
<div>
<h2>
Active sessions for [<b>{account.login}</b>]
</h2>
{updateSuccess ? (
<Alert color="success">
<strong>Session invalidated!</strong>
</Alert>
) : null}
{updateFailure ? (
<Alert color="danger">
<span>
<strong>An error has occured!</strong> The session could not be invalidated.
</span>
</Alert>
) : null}
<Button color="primary" onClick={this.refreshList}>
Refresh
</Button>
<div className="table-responsive">
<Table className="table-striped">
<thead>
<tr>
<th>IP Address</th>
<th>User agent</th>
<th>Date</th>
<th />
</tr>
</thead>
<tbody>
{sessions.map(s => (
<tr>
<td>{s.ipAddress}</td>
<td>{s.userAgent}</td>
<td>{s.tokenDate}</td>
<td>
<Button color="primary" onClick={this.doSessionInvalidation(s.series)}>
Invalidate
</Button>
</td>
</tr>
))}
</tbody>
</Table>
</div>
</div>
);
}
}
const mapStateToProps = ({ authentication, sessions }: IRootState) => ({
account: authentication.account,
sessions: sessions.sessions,
updateSuccess: sessions.updateSuccess,
updateFailure: sessions.updateFailure
});
const mapDispatchToProps = { getSession, findAll, invalidateSession };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(SessionsPage);

View File

@@ -0,0 +1,69 @@
import axios from 'axios';
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
import { getSession } from 'app/shared/reducers/authentication';
export const ACTION_TYPES = {
UPDATE_ACCOUNT: 'account/UPDATE_ACCOUNT',
RESET: 'account/RESET'
};
const initialState = {
loading: false,
errorMessage: null,
updateSuccess: false,
updateFailure: false
};
export type SettingsState = Readonly<typeof initialState>;
// Reducer
export default (state: SettingsState = initialState, action): SettingsState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.UPDATE_ACCOUNT):
return {
...state,
errorMessage: null,
updateSuccess: false,
loading: true
};
case FAILURE(ACTION_TYPES.UPDATE_ACCOUNT):
return {
...state,
loading: false,
updateSuccess: false,
updateFailure: true
};
case SUCCESS(ACTION_TYPES.UPDATE_ACCOUNT):
return {
...state,
loading: false,
updateSuccess: true,
updateFailure: false
};
case ACTION_TYPES.RESET:
return {
...initialState
};
default:
return state;
}
};
// Actions
const apiUrl = 'api/account';
export const saveAccountSettings = account => async dispatch => {
await dispatch({
type: ACTION_TYPES.UPDATE_ACCOUNT,
payload: axios.post(apiUrl, account),
meta: {
successMessage: '<strong>Settings saved!</strong>'
}
});
dispatch(getSession());
};
export const reset = () => ({
type: ACTION_TYPES.RESET
});

View File

@@ -0,0 +1,110 @@
import React from 'react';
import { Button, Col, Alert, Row } from 'reactstrap';
import { connect } from 'react-redux';
import { AvForm, AvField } from 'availity-reactstrap-validation';
import { IRootState } from 'app/shared/reducers';
import { getSession } from 'app/shared/reducers/authentication';
import { saveAccountSettings, reset } from './settings.reducer';
export interface IUserSettingsProps extends StateProps, DispatchProps {}
export interface IUserSettingsState {
account: any;
}
export class SettingsPage extends React.Component<IUserSettingsProps, IUserSettingsState> {
componentDidMount() {
this.props.getSession();
}
componentWillUnmount() {
this.props.reset();
}
handleValidSubmit = (event, values) => {
const account = {
...this.props.account,
...values
};
this.props.saveAccountSettings(account);
event.persist();
};
render() {
const { account } = this.props;
return (
<div>
<Row className="justify-content-center">
<Col md="8">
<h2 id="settings-title">User settings for {account.login}</h2>
<AvForm id="settings-form" onValidSubmit={this.handleValidSubmit}>
{/* First name */}
<AvField
className="form-control"
name="firstName"
label="First Name"
id="firstName"
placeholder="Your first name"
validate={{
required: { value: true, errorMessage: 'Your first name is required.' },
minLength: { value: 1, errorMessage: 'Your first name is required to be at least 1 character' },
maxLength: { value: 50, errorMessage: 'Your first name cannot be longer than 50 characters' }
}}
value={account.firstName}
/>
{/* Last name */}
<AvField
className="form-control"
name="lastName"
label="Last Name"
id="lastName"
placeholder="Your last name"
validate={{
required: { value: true, errorMessage: 'Your last name is required.' },
minLength: { value: 1, errorMessage: 'Your last name is required to be at least 1 character' },
maxLength: { value: 50, errorMessage: 'Your last name cannot be longer than 50 characters' }
}}
value={account.lastName}
/>
{/* Email */}
<AvField
name="email"
label="Email"
placeholder="Your email"
type="email"
validate={{
required: { value: true, errorMessage: 'Your email is required.' },
minLength: { value: 5, errorMessage: 'Your email is required to be at least 5 characters.' },
maxLength: { value: 254, errorMessage: 'Your email cannot be longer than 50 characters.' }
}}
value={account.email}
/>
<Button color="primary" type="submit">
Save
</Button>
</AvForm>
</Col>
</Row>
</div>
);
}
}
const mapStateToProps = ({ authentication }: IRootState) => ({
account: authentication.account,
isAuthenticated: authentication.isAuthenticated
});
const mapDispatchToProps = { getSession, saveAccountSettings, reset };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(SettingsPage);

View File

@@ -0,0 +1,177 @@
import axios from 'axios';
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
export const ACTION_TYPES = {
FETCH_LOGS: 'administration/FETCH_LOGS',
FETCH_LOGS_CHANGE_LEVEL: 'administration/FETCH_LOGS_CHANGE_LEVEL',
FETCH_HEALTH: 'administration/FETCH_HEALTH',
FETCH_METRICS: 'administration/FETCH_METRICS',
FETCH_THREAD_DUMP: 'administration/FETCH_THREAD_DUMP',
FETCH_CONFIGURATIONS: 'administration/FETCH_CONFIGURATIONS',
FETCH_ENV: 'administration/FETCH_ENV',
FETCH_AUDITS: 'administration/FETCH_AUDITS'
};
const initialState = {
loading: false,
errorMessage: null,
logs: {
loggers: [] as any[]
},
health: {} as any,
metrics: {} as any,
threadDump: [],
configuration: {
configProps: {} as any,
env: {} as any
},
audits: [],
totalItems: 0
};
export type AdministrationState = Readonly<typeof initialState>;
// Reducer
export default (state: AdministrationState = initialState, action): AdministrationState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.FETCH_METRICS):
case REQUEST(ACTION_TYPES.FETCH_THREAD_DUMP):
case REQUEST(ACTION_TYPES.FETCH_LOGS):
case REQUEST(ACTION_TYPES.FETCH_CONFIGURATIONS):
case REQUEST(ACTION_TYPES.FETCH_ENV):
case REQUEST(ACTION_TYPES.FETCH_AUDITS):
case REQUEST(ACTION_TYPES.FETCH_HEALTH):
return {
...state,
errorMessage: null,
loading: true
};
case FAILURE(ACTION_TYPES.FETCH_METRICS):
case FAILURE(ACTION_TYPES.FETCH_THREAD_DUMP):
case FAILURE(ACTION_TYPES.FETCH_LOGS):
case FAILURE(ACTION_TYPES.FETCH_CONFIGURATIONS):
case FAILURE(ACTION_TYPES.FETCH_ENV):
case FAILURE(ACTION_TYPES.FETCH_AUDITS):
case FAILURE(ACTION_TYPES.FETCH_HEALTH):
return {
...state,
loading: false,
errorMessage: action.payload
};
case SUCCESS(ACTION_TYPES.FETCH_METRICS):
return {
...state,
loading: false,
metrics: action.payload.data
};
case SUCCESS(ACTION_TYPES.FETCH_THREAD_DUMP):
return {
...state,
loading: false,
threadDump: action.payload.data
};
case SUCCESS(ACTION_TYPES.FETCH_LOGS):
return {
...state,
loading: false,
logs: {
loggers: action.payload.data
}
};
case SUCCESS(ACTION_TYPES.FETCH_CONFIGURATIONS):
return {
...state,
loading: false,
configuration: {
...state.configuration,
configProps: action.payload.data
}
};
case SUCCESS(ACTION_TYPES.FETCH_ENV):
return {
...state,
loading: false,
configuration: {
...state.configuration,
env: action.payload.data
}
};
case SUCCESS(ACTION_TYPES.FETCH_AUDITS):
return {
...state,
loading: false,
audits: action.payload.data,
totalItems: action.payload.headers['x-total-count']
};
case SUCCESS(ACTION_TYPES.FETCH_HEALTH):
return {
...state,
loading: false,
health: action.payload.data
};
default:
return state;
}
};
// Actions
export const systemHealth = () => ({
type: ACTION_TYPES.FETCH_HEALTH,
payload: axios.get('management/health')
});
export const systemMetrics = () => ({
type: ACTION_TYPES.FETCH_METRICS,
payload: axios.get('management/metrics')
});
export const systemThreadDump = () => ({
type: ACTION_TYPES.FETCH_THREAD_DUMP,
payload: axios.get('management/threaddump')
});
export const getLoggers = () => ({
type: ACTION_TYPES.FETCH_LOGS,
payload: axios.get('management/logs')
});
export const changeLogLevel = (name, level) => {
const body = {
level,
name
};
return async dispatch => {
await dispatch({
type: ACTION_TYPES.FETCH_LOGS_CHANGE_LEVEL,
payload: axios.put('management/logs', body)
});
dispatch(getLoggers());
};
};
export const getConfigurations = () => ({
type: ACTION_TYPES.FETCH_CONFIGURATIONS,
payload: axios.get('management/configprops')
});
export const getEnv = () => ({
type: ACTION_TYPES.FETCH_ENV,
payload: axios.get('management/env')
});
export const getAudits = (page, size, sort, fromDate, toDate) => {
let requestUrl = `management/audits${sort ? `?page=${page}&size=${size}&sort=${sort}` : ''}`;
if (fromDate) {
requestUrl += `&fromDate=${fromDate}`;
}
if (toDate) {
requestUrl += `&toDate=${toDate}`;
}
return {
type: ACTION_TYPES.FETCH_AUDITS,
payload: axios.get(requestUrl)
};
};

View File

@@ -0,0 +1,156 @@
import React from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router';
import { Input, Row, Table } from 'reactstrap';
import { TextFormat, JhiPagination, getPaginationItemsNumber, getSortState, IPaginationBaseState } from 'react-jhipster';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { APP_TIMESTAMP_FORMAT } from 'app/config/constants';
import { ITEMS_PER_PAGE } from 'app/shared/util/pagination.constants';
import { IRootState } from 'app/shared/reducers';
import { getAudits } from '../administration.reducer';
export interface IAuditsPageProps extends StateProps, DispatchProps, RouteComponentProps<{}> {}
export interface IAuditsPageState extends IPaginationBaseState {
fromDate: string;
toDate: string;
}
const previousMonth = (): string => {
const now: Date = new Date();
const fromDate =
now.getMonth() === 0
? new Date(now.getFullYear() - 1, 11, now.getDate())
: new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
return fromDate.toISOString().slice(0, 10);
};
const today = (): string => {
// Today + 1 day - needed if the current day must be included
const day: Date = new Date();
day.setDate(day.getDate() + 1);
const toDate = new Date(day.getFullYear(), day.getMonth(), day.getDate());
return toDate.toISOString().slice(0, 10);
};
export class AuditsPage extends React.Component<IAuditsPageProps, IAuditsPageState> {
state: IAuditsPageState = {
...getSortState(this.props.location, ITEMS_PER_PAGE),
fromDate: previousMonth(),
toDate: today()
};
componentDidMount() {
this.getAudits();
}
onChangeFromDate = evt => {
this.setState(
{
fromDate: evt.target.value
},
() => this.getAudits()
);
};
onChangeToDate = evt => {
this.setState(
{
toDate: evt.target.value
},
() => this.getAudits()
);
};
sort = prop => () => {
this.setState(
{
order: this.state.order === 'asc' ? 'desc' : 'asc',
sort: prop
},
() => this.transition()
);
};
transition = () => {
this.getAudits();
this.props.history.push(`${this.props.location.pathname}?page=${this.state.activePage}&sort=${this.state.sort},${this.state.order}`);
};
handlePagination = activePage => this.setState({ activePage }, () => this.transition());
getAudits = () => {
const { activePage, itemsPerPage, sort, order, fromDate, toDate } = this.state;
this.props.getAudits(activePage - 1, itemsPerPage, `${sort},${order}`, fromDate, toDate);
};
render() {
const { audits, totalItems } = this.props;
const { fromDate, toDate } = this.state;
return (
<div>
<h2 className="audits-page-heading">Audits</h2>
<span>from</span>
<Input type="date" value={fromDate} onChange={this.onChangeFromDate} name="fromDate" id="fromDate" />
<span>to</span>
<Input type="date" value={toDate} onChange={this.onChangeToDate} name="toDate" id="toDate" />
<Table striped responsive>
<thead>
<tr>
<th onClick={this.sort('auditEventDate')}>
Date
<FontAwesomeIcon icon="sort" />
</th>
<th onClick={this.sort('principal')}>
User
<FontAwesomeIcon icon="sort" />
</th>
<th onClick={this.sort('auditEventType')}>
State
<FontAwesomeIcon icon="sort" />
</th>
<th>Extra data</th>
</tr>
</thead>
<tbody>
{audits.map((audit, i) => (
<tr key={`audit-${i}`}>
<td>{<TextFormat value={audit.timestamp} type="date" format={APP_TIMESTAMP_FORMAT} />}</td>
<td>{audit.principal}</td>
<td>{audit.type}</td>
<td>
{audit.data ? audit.data.message : null}
{audit.data ? audit.data.remoteAddress : null}
</td>
</tr>
))}
</tbody>
</Table>
<Row className="justify-content-center">
<JhiPagination
items={getPaginationItemsNumber(totalItems, this.state.itemsPerPage)}
activePage={this.state.activePage}
onSelect={this.handlePagination}
maxButtons={5}
/>
</Row>
</div>
);
}
}
const mapStateToProps = (storeState: IRootState) => ({
audits: storeState.administration.audits,
totalItems: storeState.administration.totalItems
});
const mapDispatchToProps = { getAudits };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(AuditsPage);

View File

@@ -0,0 +1,140 @@
import React from 'react';
import { connect } from 'react-redux';
import { Table, Input, Row, Col, Badge } from 'reactstrap';
import { getConfigurations, getEnv } from '../administration.reducer';
import { IRootState } from 'app/shared/reducers';
export interface IConfigurationPageProps extends StateProps, DispatchProps {}
export interface IConfigurationPageState {
filter: string;
reversePrefix: boolean;
reverseProperties: boolean;
}
export class ConfigurationPage extends React.Component<IConfigurationPageProps, IConfigurationPageState> {
state: IConfigurationPageState = {
filter: '',
reversePrefix: false,
reverseProperties: false
};
componentDidMount() {
this.props.getConfigurations();
this.props.getEnv();
}
setFilter = evt => {
this.setState({
filter: evt.target.value
});
};
envFilterFn = configProp => configProp.toUpperCase().includes(this.state.filter.toUpperCase());
propsFilterFn = configProp => configProp.prefix.toUpperCase().includes(this.state.filter.toUpperCase());
reversePrefix = () => {
this.setState({
reversePrefix: !this.state.reversePrefix
});
};
reverseProperties = () => {
this.setState({
reverseProperties: !this.state.reverseProperties
});
};
getContextList = contexts =>
Object.values(contexts)
.map((v: any) => v.beans)
.reduce((acc, e) => ({ ...acc, ...e }));
render() {
const { configuration } = this.props;
const { filter } = this.state;
const configProps = configuration && configuration.configProps ? configuration.configProps : {};
const env = configuration && configuration.env ? configuration.env : {};
return (
<div>
<h2 className="configuration-page-heading">Configuration</h2>
<span>Filter</span> <Input type="search" value={filter} onChange={this.setFilter} name="search" id="search" />
<label>Spring configuration</label>
<Table className="table table-striped table-bordered table-responsive d-table">
<thead>
<tr>
<th onClick={this.reversePrefix}>Prefix</th>
<th onClick={this.reverseProperties}>Properties</th>
</tr>
</thead>
<tbody>
{configProps.contexts
? Object.values(this.getContextList(configProps.contexts))
.filter(this.propsFilterFn)
.map((property, propIndex) => (
<tr key={propIndex}>
<td>{property['prefix']}</td>
<td>
{Object.keys(property['properties']).map((propKey, index) => (
<Row key={index}>
<Col md="4">{propKey}</Col>
<Col md="8">
<Badge className="float-right badge-secondary break">{JSON.stringify(property['properties'][propKey])}</Badge>
</Col>
</Row>
))}
</td>
</tr>
))
: null}
</tbody>
</Table>
{env.propertySources
? env.propertySources.map((envKey, envIndex) => (
<div key={envIndex}>
<h4>
<span>{envKey.name}</span>
</h4>
<Table className="table table-sm table-striped table-bordered table-responsive d-table">
<thead>
<tr key={envIndex}>
<th className="w-40">Property</th>
<th className="w-60">Value</th>
</tr>
</thead>
<tbody>
{Object.keys(envKey.properties)
.filter(this.envFilterFn)
.map((propKey, propIndex) => (
<tr key={propIndex}>
<td className="break">{propKey}</td>
<td className="break">
<span className="float-right badge badge-secondary break">{envKey.properties[propKey].value}</span>
</td>
</tr>
))}
</tbody>
</Table>
</div>
))
: null}
</div>
);
}
}
const mapStateToProps = ({ administration }: IRootState) => ({
configuration: administration.configuration,
isFetching: administration.loading
});
const mapDispatchToProps = { getConfigurations, getEnv };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(ConfigurationPage);

View File

@@ -0,0 +1,9 @@
import React from 'react';
const DocsPage = () => (
<div>
<iframe src="../swagger-ui/index.html" width="100%" height="800" title="Swagger UI" seamless style={{ border: 'none' }} />
</div>
);
export default DocsPage;

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { Table, Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
const formatDiskSpaceOutput = rawValue => {
// Should display storage space in an human readable unit
const val = rawValue / 1073741824;
if (val > 1) {
// Value
return val.toFixed(2) + ' GB';
} else {
return (rawValue / 1048576).toFixed(2) + ' MB';
}
};
const HealthModal = ({ handleClose, healthObject, showModal }) => {
const data = healthObject.details || {};
return (
<Modal isOpen={showModal} modalTransition={{ timeout: 20 }} backdropTransition={{ timeout: 10 }} toggle={handleClose}>
<ModalHeader toggle={handleClose}>{healthObject.name}</ModalHeader>
<ModalBody>
<Table bordered>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{Object.keys(data).map((key, index) => (
<tr key={index}>
<td>{key}</td>
<td>{healthObject.name === 'diskSpace' ? formatDiskSpaceOutput(data[key]) : JSON.stringify(data[key])}</td>
</tr>
))}
</tbody>
</Table>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={handleClose}>
Close
</Button>
</ModalFooter>
</Modal>
);
};
export default HealthModal;

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { connect } from 'react-redux';
import { Table, Badge, Col, Row, Button } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IRootState } from 'app/shared/reducers';
import { systemHealth } from '../administration.reducer';
import HealthModal from './health-modal';
export interface IHealthPageProps extends StateProps, DispatchProps {}
export interface IHealthPageState {
healthObject: any;
showModal: boolean;
}
export class HealthPage extends React.Component<IHealthPageProps, IHealthPageState> {
state: IHealthPageState = {
healthObject: {},
showModal: false
};
componentDidMount() {
this.props.systemHealth();
}
getSystemHealth = () => {
if (!this.props.isFetching) {
this.props.systemHealth();
}
};
getSystemHealthInfo = (name, healthObject) => () => {
this.setState({
showModal: true,
healthObject: {
...healthObject,
name
}
});
};
handleClose = () => {
this.setState({
showModal: false
});
};
renderModal = () => {
const { healthObject } = this.state;
return <HealthModal healthObject={healthObject} handleClose={this.handleClose} showModal={this.state.showModal} />;
};
render() {
const { health, isFetching } = this.props;
const data = (health || {}).details || {};
return (
<div>
<h2 className="health-page-heading">Health Checks</h2>
<p>
<Button onClick={this.getSystemHealth} color={isFetching ? 'btn btn-danger' : 'btn btn-primary'} disabled={isFetching}>
<FontAwesomeIcon icon="sync" />&nbsp; Refresh
</Button>
</p>
<Row>
<Col md="12">
<Table bordered>
<thead>
<tr>
<th>Service Name</th>
<th>Status</th>
<th>Details</th>
</tr>
</thead>
<tbody>
{Object.keys(data).map(
(configPropKey, configPropIndex) =>
configPropKey !== 'status' ? (
<tr key={configPropIndex}>
<td>{configPropKey}</td>
<td>
<Badge color={data[configPropKey].status !== 'UP' ? 'danger' : 'success'}>{data[configPropKey].status}</Badge>
</td>
<td>
{data[configPropKey].details ? (
<a onClick={this.getSystemHealthInfo(configPropKey, data[configPropKey])}>
<FontAwesomeIcon icon="eye" />
</a>
) : null}
</td>
</tr>
) : null
)}
</tbody>
</Table>
</Col>
</Row>
{this.renderModal()}
</div>
);
}
}
const mapStateToProps = (storeState: IRootState) => ({
health: storeState.administration.health,
isFetching: storeState.administration.loading
});
const mapDispatchToProps = { systemHealth };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(HealthPage);

View File

@@ -0,0 +1,24 @@
import React from 'react';
import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route';
import UserManagement from './user-management';
import Logs from './logs/logs';
import Health from './health/health';
import Metrics from './metrics/metrics';
import Configuration from './configuration/configuration';
import Audits from './audits/audits';
import Docs from './docs/docs';
const Routes = ({ match }) => (
<div>
<ErrorBoundaryRoute path={`${match.url}/user-management`} component={UserManagement} />
<ErrorBoundaryRoute exact path={`${match.url}/health`} component={Health} />
<ErrorBoundaryRoute exact path={`${match.url}/metrics`} component={Metrics} />
<ErrorBoundaryRoute exact path={`${match.url}/docs`} component={Docs} />
<ErrorBoundaryRoute exact path={`${match.url}/configuration`} component={Configuration} />
<ErrorBoundaryRoute exact path={`${match.url}/audits`} component={Audits} />
<ErrorBoundaryRoute exact path={`${match.url}/logs`} component={Logs} />
</div>
);
export default Routes;

View File

@@ -0,0 +1,137 @@
import React from 'react';
import { connect } from 'react-redux';
import { getLoggers, changeLogLevel } from '../administration.reducer';
import { IRootState } from 'app/shared/reducers';
export interface ILogsPageProps extends StateProps, DispatchProps {}
export interface ILogsPageState {
filter: string;
}
export class LogsPage extends React.Component<ILogsPageProps, ILogsPageState> {
state: ILogsPageState = {
filter: ''
};
componentDidMount() {
this.props.getLoggers();
}
getLogs = () => {
if (!this.props.isFetching) {
this.props.getLoggers();
}
};
changeLevel = (loggerName, level) => () => {
this.props.changeLogLevel(loggerName, level);
};
setFilter = evt => {
this.setState({
filter: evt.target.value
});
};
getClassName = (level, check, className) => (level === check ? `btn btn-sm btn-${className}` : 'btn btn-sm btn-light');
filterFn = l => l.name.toUpperCase().includes(this.state.filter.toUpperCase());
render() {
const { logs, isFetching } = this.props;
const { filter } = this.state;
const loggers = logs ? logs.loggers : [];
return (
<div>
<h2 className="logs-page-heading">Logs</h2>
<p>There are {loggers.length.toString()} loggers.</p>
<span>Filter</span>
<input type="text" value={filter} onChange={this.setFilter} className="form-control" disabled={isFetching} />
<table className="table table-sm table-striped table-bordered">
<thead>
<tr title="click to order">
<th>
<span>Name</span>
</th>
<th>
<span>Level</span>
</th>
</tr>
</thead>
<tbody>
{loggers.filter(this.filterFn).map((logger, i) => (
<tr key={`log-row-${i}`}>
<td>
<small>{logger.name}</small>
</td>
<td>
<button
disabled={isFetching}
onClick={this.changeLevel(logger.name, 'TRACE')}
className={this.getClassName(logger.level, 'TRACE', 'primary')}
>
TRACE
</button>
<button
disabled={isFetching}
onClick={this.changeLevel(logger.name, 'DEBUG')}
className={this.getClassName(logger.level, 'DEBUG', 'success')}
>
DEBUG
</button>
<button
disabled={isFetching}
onClick={this.changeLevel(logger.name, 'INFO')}
className={this.getClassName(logger.level, 'INFO', 'info')}
>
INFO
</button>
<button
disabled={isFetching}
onClick={this.changeLevel(logger.name, 'WARN')}
className={this.getClassName(logger.level, 'WARN', 'warning')}
>
WARN
</button>
<button
disabled={isFetching}
onClick={this.changeLevel(logger.name, 'ERROR')}
className={this.getClassName(logger.level, 'ERROR', 'danger')}
>
ERROR
</button>
<button
disabled={isFetching}
onClick={this.changeLevel(logger.name, 'OFF')}
className={this.getClassName(logger.level, 'OFF', 'secondary')}
>
OFF
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
}
const mapStateToProps = ({ administration }: IRootState) => ({
logs: administration.logs,
isFetching: administration.loading
});
const mapDispatchToProps = { getLoggers, changeLogLevel };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(LogsPage);

View File

@@ -0,0 +1,165 @@
import React from 'react';
import { Table, Modal, ModalHeader, ModalBody, ModalFooter, Button, Input, Badge, Row } from 'reactstrap';
import ThreadItem from './thread-item';
export interface IMetricsModalProps {
showModal: boolean;
handleClose: Function;
threadDump: any;
}
export interface IMetricsModalState {
badgeFilter: string;
searchFilter: string;
}
export class MetricsModal extends React.Component<IMetricsModalProps, IMetricsModalState> {
state: IMetricsModalState = {
badgeFilter: '',
searchFilter: ''
};
computeFilteredList = () => {
const { badgeFilter, searchFilter } = this.state;
let filteredList = this.props.threadDump.threads;
if (badgeFilter !== '') {
filteredList = filteredList.filter(t => t.threadState === badgeFilter);
}
if (searchFilter !== '') {
filteredList = filteredList.filter(t => t.lockName && t.lockName.toLowerCase().includes(searchFilter.toLowerCase()));
}
return filteredList;
};
computeCounters = () => {
let threadDumpAll = 0;
let threadDumpRunnable = 0;
let threadDumpWaiting = 0;
let threadDumpTimedWaiting = 0;
let threadDumpBlocked = 0;
this.props.threadDump.threads.forEach(t => {
switch (t.threadState) {
case 'RUNNABLE':
threadDumpRunnable++;
break;
case 'WAITING':
threadDumpWaiting++;
break;
case 'TIMED_WAITING':
threadDumpTimedWaiting++;
break;
case 'BLOCKED':
threadDumpBlocked++;
break;
default:
break;
}
});
threadDumpAll = threadDumpRunnable + threadDumpWaiting + threadDumpTimedWaiting + threadDumpBlocked;
return { threadDumpAll, threadDumpRunnable, threadDumpWaiting, threadDumpTimedWaiting, threadDumpBlocked };
};
getBadgeClass = threadState => {
if (threadState === 'RUNNABLE') {
return 'badge-success';
} else if (threadState === 'WAITING') {
return 'badge-info';
} else if (threadState === 'TIMED_WAITING') {
return 'badge-warning';
} else if (threadState === 'BLOCKED') {
return 'badge-danger';
}
};
updateBadgeFilter = badge => () => this.setState({ badgeFilter: badge });
updateSearchFilter = event => this.setState({ searchFilter: event.target.value });
render() {
const { showModal, handleClose, threadDump } = this.props;
let counters = {} as any;
let filteredList = null;
if (threadDump && threadDump.threads) {
counters = this.computeCounters();
filteredList = this.computeFilteredList();
}
return (
<Modal isOpen={showModal} toggle={handleClose} className="modal-lg">
<ModalHeader toggle={handleClose}>Threads dump</ModalHeader>
<ModalBody>
<Badge color="primary" className="hand" onClick={this.updateBadgeFilter('')}>
All&nbsp;
<Badge pill>{counters.threadDumpAll || 0}</Badge>
</Badge>&nbsp;
<Badge color="success" className="hand" onClick={this.updateBadgeFilter('RUNNABLE')}>
Runnable&nbsp;
<Badge pill>{counters.threadDumpRunnable || 0}</Badge>
</Badge>&nbsp;
<Badge color="info" className="hand" onClick={this.updateBadgeFilter('WAITING')}>
Waiting&nbsp;
<Badge pill>{counters.threadDumpWaiting || 0}</Badge>
</Badge>&nbsp;
<Badge color="warning" className="hand" onClick={this.updateBadgeFilter('TIMED_WAITING')}>
Timed Waiting&nbsp;
<Badge pill>{counters.threadDumpTimedWaiting || 0}</Badge>
</Badge>&nbsp;
<Badge color="danger" className="hand" onClick={this.updateBadgeFilter('BLOCKED')}>
Blocked&nbsp;
<Badge pill>{counters.threadDumpBlocked || 0}</Badge>
</Badge>&nbsp;
<div className="mt-2">&nbsp;</div>
<Input type="text" className="form-control" placeholder="Filter by Lock Name..." onChange={this.updateSearchFilter} />
<div style={{ padding: '10px' }}>
{filteredList
? filteredList.map((threadDumpInfo, i) => (
<div key={`dump-${i}`}>
<h6>
{' '}
<span className={'badge ' + this.getBadgeClass(threadDumpInfo.threadState)}>{threadDumpInfo.threadState}</span>&nbsp;
{threadDumpInfo.threadName} (ID {threadDumpInfo.threadId})&nbsp;
</h6>
<ThreadItem threadDumpInfo={threadDumpInfo} />
<Row>
<Table responsive>
<thead>
<tr>
<th>Blocked Time</th>
<th>Blocked Count</th>
<th>Waited Time</th>
<th>Waited Count</th>
<th>Lock Name</th>
</tr>
</thead>
<tbody>
<tr key={threadDumpInfo.lockName}>
<td>{threadDumpInfo.blockedTime}</td>
<td>{threadDumpInfo.blockedCount}</td>
<td>{threadDumpInfo.waitedTime}</td>
<td>{threadDumpInfo.waitedCount}</td>
<td className="thread-dump-modal-lock" title={threadDumpInfo.lockName}>
<code>{threadDumpInfo.lockName}</code>
</td>
</tr>
</tbody>
</Table>
</Row>
</div>
))
: null}
</div>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={handleClose}>
Close
</Button>
</ModalFooter>
</Modal>
);
}
}
export default MetricsModal;

View File

@@ -0,0 +1,672 @@
import React from 'react';
import { connect } from 'react-redux';
import { Table, Progress, Col, Row, Button } from 'reactstrap';
import { TextFormat } from 'react-jhipster';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { APP_WHOLE_NUMBER_FORMAT, APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT } from 'app/config/constants';
import { systemMetrics, systemThreadDump } from '../administration.reducer';
import MetricsModal from './metrics-modal';
import { IRootState } from 'app/shared/reducers';
export interface IMetricsPageProps extends StateProps, DispatchProps {}
export interface IMetricsPageState {
showModal: boolean;
}
export class MetricsPage extends React.Component<IMetricsPageProps, IMetricsPageState> {
state: IMetricsPageState = {
showModal: false
};
componentDidMount() {
this.props.systemMetrics();
}
getMetrics = () => {
if (!this.props.isFetching) {
this.props.systemMetrics();
}
};
getThreadDump = () => {
this.props.systemThreadDump();
this.setState({
showModal: true
});
};
handleClose = () => {
this.setState({
showModal: false
});
};
filterNaN = input => (isNaN(input) ? 0 : input);
getStats = metrics => {
const stat = {
servicesStats: {},
cachesStats: {}
};
if (!this.props.isFetching && metrics && metrics.timers) {
Object.keys(metrics.timers).forEach((key, indexNm) => {
if (key.indexOf('web.rest') !== -1 || key.indexOf('service') !== -1) {
stat.servicesStats[key] = metrics.timers[key];
}
if (key.indexOf('net.sf.ehcache.Cache') !== -1) {
// remove gets or puts
const index = key.lastIndexOf('.');
const newKey = key.substr(0, index);
// Keep the name of the domain
stat.cachesStats[newKey] = {
name: newKey,
value: metrics.timers[key]
};
}
});
}
return stat;
};
gaugeRow = (metrics, label: String, key) =>
metrics.gauges[key] ? (
<Row>
<Col md="9">{label}</Col>
<Col md="3" className="text-right">
{metrics.gauges[key].value}
</Col>
</Row>
) : null;
renderModal = () => <MetricsModal handleClose={this.handleClose} showModal={this.state.showModal} threadDump={this.props.threadDump} />;
renderGauges = metrics => (
<Row>
<Col sm="12">
<h3>JVM Metrics</h3>
<Row>
<Col md="4">
<b>Memory</b>
<p>
<span>Total Memory</span> (
<TextFormat value={metrics.gauges['jvm.memory.total.used'].value / 1048576} type="number" format={APP_WHOLE_NUMBER_FORMAT} />
M /{' '}
<TextFormat value={metrics.gauges['jvm.memory.total.max'].value / 1048576} type="number" format={APP_WHOLE_NUMBER_FORMAT} />
M)
</p>
<Progress
animated
value={metrics.gauges['jvm.memory.total.used'].value}
min="0"
max={metrics.gauges['jvm.memory.total.max'].value}
color="success"
>
<span>
<TextFormat
value={(metrics.gauges['jvm.memory.total.used'].value * 100) / metrics.gauges['jvm.memory.total.max'].value}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
%
</span>
</Progress>
<p>
<span>Heap Memory</span> (
<TextFormat value={metrics.gauges['jvm.memory.heap.used'].value / 1048576} type="number" format={APP_WHOLE_NUMBER_FORMAT} />
M /{' '}
<TextFormat value={metrics.gauges['jvm.memory.heap.max'].value / 1048576} type="number" format={APP_WHOLE_NUMBER_FORMAT} />
M)
</p>
<Progress
animated
min="0"
max={metrics.gauges['jvm.memory.heap.max'].value}
value={metrics.gauges['jvm.memory.heap.used'].value}
color="success"
>
<span>
<TextFormat
value={(metrics.gauges['jvm.memory.heap.used'].value * 100) / metrics.gauges['jvm.memory.heap.max'].value}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
%
</span>
</Progress>
<p>
<span>Non-Heap Memory</span> (
<TextFormat
value={metrics.gauges['jvm.memory.non-heap.used'].value / 1048576}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
M /{' '}
<TextFormat
value={metrics.gauges['jvm.memory.non-heap.committed'].value / 1048576}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
M)
</p>
<Progress
animated
min="0"
max={metrics.gauges['jvm.memory.non-heap.committed'].value}
value={metrics.gauges['jvm.memory.non-heap.used'].value}
color="success"
>
<span>
<TextFormat
value={(metrics.gauges['jvm.memory.non-heap.used'].value * 100) / metrics.gauges['jvm.memory.non-heap.committed'].value}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
%
</span>
</Progress>
</Col>
<Col md="4">
<b>Threads</b> (Total: {metrics.gauges['jvm.threads.count'].value}){' '}
<Button color="link" className="hand" onClick={this.getThreadDump}>
<FontAwesomeIcon icon="eye" />
</Button>
<p>
<span>Runnable</span> {metrics.gauges['jvm.threads.runnable.count'].value}
</p>
<Progress
animated
min="0"
value={metrics.gauges['jvm.threads.runnable.count'].value}
max={metrics.gauges['jvm.threads.count'].value}
color="success"
>
<span>
<TextFormat
value={(metrics.gauges['jvm.threads.runnable.count'].value * 100) / metrics.gauges['jvm.threads.count'].value}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
%
</span>
</Progress>
<p>
<span>Timed Waiting</span> ({metrics.gauges['jvm.threads.timed_waiting.count'].value})
</p>
<Progress
animated
min="0"
value={metrics.gauges['jvm.threads.timed_waiting.count'].value}
max={metrics.gauges['jvm.threads.count'].value}
color="warning"
>
<span>
<TextFormat
value={(metrics.gauges['jvm.threads.timed_waiting.count'].value * 100) / metrics.gauges['jvm.threads.count'].value}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
%
</span>
</Progress>
<p>
<span>Waiting</span> ({metrics.gauges['jvm.threads.waiting.count'].value})
</p>
<Progress
animated
min="0"
value={metrics.gauges['jvm.threads.waiting.count'].value}
max={metrics.gauges['jvm.threads.count'].value}
color="warning"
>
<span>
<TextFormat
value={(metrics.gauges['jvm.threads.waiting.count'].value * 100) / metrics.gauges['jvm.threads.count'].value}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
%
</span>
</Progress>
<p>
<span>Blocked</span> ({metrics.gauges['jvm.threads.blocked.count'].value})
</p>
<Progress
animated
min="0"
value={metrics.gauges['jvm.threads.blocked.count'].value}
max={metrics.gauges['jvm.threads.count'].value}
color="success"
>
<span>
<TextFormat
value={(metrics.gauges['jvm.threads.blocked.count'].value * 100) / metrics.gauges['jvm.threads.count'].value}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
%
</span>
</Progress>
</Col>
<Col md="4">
<b>Garbage collections</b>
{this.gaugeRow(metrics, 'Mark Sweep count', 'jvm.garbage.PS-MarkSweep.count')}
{this.gaugeRow(metrics, 'Mark Sweep time', 'jvm.garbage.PS-MarkSweep.time')}
{this.gaugeRow(metrics, 'Scavenge count', 'jvm.garbage.PS-Scavenge.count')}
{this.gaugeRow(metrics, 'Scavenge time', 'jvm.garbage.PS-Scavenge.time')}
</Col>
</Row>
</Col>
</Row>
);
render() {
const { metrics, isFetching } = this.props;
const data = metrics || {};
const { servicesStats, cachesStats } = this.getStats(data);
return (
<div>
<h2 className="metrics-page-heading">Application Metrics</h2>
<p>
<Button onClick={this.getMetrics} color={isFetching ? 'btn btn-danger' : 'btn btn-primary'} disabled={isFetching}>
<FontAwesomeIcon icon="sync" />&nbsp; Refresh
</Button>
</p>
<hr />
{metrics.gauges ? this.renderGauges(metrics) : ''}
{metrics.meters && metrics.timers ? (
<Row>
<Col sm="12">
<h3>HTTP requests (events per second)</h3>
<p>
<span>Active requests:</span>{' '}
<b>
<TextFormat
value={metrics.counters['com.codahale.metrics.servlet.InstrumentedFilter.activeRequests'].count}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
</b>{' '}
- <span>Total requests:</span>{' '}
<b>
<TextFormat
value={metrics.timers['com.codahale.metrics.servlet.InstrumentedFilter.requests'].count}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
</b>
</p>
<Table>
<thead>
<tr>
<th>Code</th>
<th>Count</th>
<th className="text-right">Mean</th>
<th className="text-right">
<span>Average</span> (1 min)
</th>
<th className="text-right">
<span>Average</span> (5 min)
</th>
<th className="text-right">
<span>Average</span> (15 min)
</th>
</tr>
</thead>
<tbody>
<tr key={0}>
<td>OK</td>
<td>
<Progress
min="0"
max={metrics.timers['com.codahale.metrics.servlet.InstrumentedFilter.requests'].count}
value={metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.ok'].count}
color="success"
animated
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.ok'].mean_rate)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.ok'].m1_rate)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.ok'].m5_rate)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.ok'].m15_rate)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
</tr>
<tr key={1}>
<td>Not Found</td>
<td>
<Progress
min="0"
max={metrics.timers['com.codahale.metrics.servlet.InstrumentedFilter.requests'].count}
value={metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.notFound'].count}
color="success"
animated
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(
metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.notFound'].mean_rate
)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(
metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.notFound'].m1_rate
)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(
metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.notFound'].m5_rate
)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(
metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.notFound'].m15_rate
)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
</tr>
<tr key={2}>
<td>Server Error</td>
<td>
<Progress
min="0"
max={metrics.timers['com.codahale.metrics.servlet.InstrumentedFilter.requests'].count}
value={metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.serverError'].count}
color="success"
animated
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(
metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.serverError'].mean_rate
)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(
metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.serverError'].m1_rate
)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(
metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.serverError'].m5_rate
)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(
metrics.meters['com.codahale.metrics.servlet.InstrumentedFilter.responseCodes.serverError'].m15_rate
)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
</tr>
</tbody>
</Table>
</Col>
</Row>
) : (
''
)}
{servicesStats ? (
<Row>
<Col sm="12">
<h3>Services statistics (time in millisecond)</h3>
</Col>
<Table>
<thead>
<tr>
<th>Service name</th>
<th>Count</th>
<th>Mean</th>
<th>Min</th>
<th>p50</th>
<th>p75</th>
<th>p95</th>
<th>p99</th>
<th>Max</th>
</tr>
</thead>
<tbody>
{Object.keys(servicesStats).map((key, index) => (
<tr key={key}>
<td>{key}</td>
<td>{servicesStats[key].count}</td>
<td>
<TextFormat value={servicesStats[key].mean * 1024} type="number" format={APP_WHOLE_NUMBER_FORMAT} />
</td>
<td>
<TextFormat value={servicesStats[key].min * 1024} type="number" format={APP_WHOLE_NUMBER_FORMAT} />
</td>
<td>
<TextFormat value={servicesStats[key].p50 * 1024} type="number" format={APP_WHOLE_NUMBER_FORMAT} />
</td>
<td>
<TextFormat value={servicesStats[key].p75 * 1024} type="number" format={APP_WHOLE_NUMBER_FORMAT} />
</td>
<td>
<TextFormat value={servicesStats[key].p95 * 1024} type="number" format={APP_WHOLE_NUMBER_FORMAT} />
</td>
<td>
<TextFormat value={servicesStats[key].p99 * 1024} type="number" format={APP_WHOLE_NUMBER_FORMAT} />
</td>
<td>
<TextFormat value={servicesStats[key].max * 1024} type="number" format={APP_WHOLE_NUMBER_FORMAT} />
</td>
</tr>
))}
</tbody>
</Table>
</Row>
) : (
''
)}
{cachesStats ? (
<Row>
<Col sm="12">
<h3>Ehcache statistics</h3>
<Table>
<thead>
<tr>
<th>Cache Name</th>
<th>Object</th>
<th>Misses</th>
<th>Eviction Count</th>
</tr>
</thead>
<tbody>
{Object.keys(cachesStats).map((k, v) => (
<tr key={k}>
<td>{k}</td>
<td>{metrics.gauges[`${k}.objects`].value}</td>
<td>{metrics.gauges[`${k}.hits`].value}</td>
<td>{metrics.gauges[`${k}.misses`].value}</td>
<td>{metrics.gauges[`${k}.eviction-count`].value}</td>
</tr>
))}
</tbody>
</Table>
</Col>
</Row>
) : (
''
)}
{metrics.gauges &&
metrics.gauges['HikariPool-1.pool.TotalConnections'] &&
metrics.gauges['HikariPool-1.pool.TotalConnections'].value > 0 ? (
<Row>
<Col sm="12">
<h3>DataSource statistics (time in millisecond)</h3>
<Table>
<thead>
<tr>
<th>
<span>Usage</span>
({metrics.gauges['HikariPool-1.pool.ActiveConnections'].value} /{' '}
{metrics.gauges['HikariPool-1.pool.TotalConnections'].value})
</th>
<th className="text-right">Count</th>
<th className="text-right">Mean</th>
<th className="text-right">Min</th>
<th className="text-right">p50</th>
<th className="text-right">p75</th>
<th className="text-right">p95</th>
<th className="text-right">p99</th>
<th className="text-right">Max</th>
</tr>
</thead>
<tbody>
<tr key="DB">
<td>
<Progress
min="0"
max={metrics.gauges['HikariPool-1.pool.TotalConnections'].value}
value={metrics.gauges['HikariPool-1.pool.ActiveConnections'].value}
>
<span>
<TextFormat
value={
(metrics.gauges['HikariPool-1.pool.ActiveConnections'].value * 100) /
metrics.gauges['HikariPool-1.pool.TotalConnections'].value
}
type="number"
format={APP_WHOLE_NUMBER_FORMAT}
/>
%
</span>
</Progress>
</td>
<td className="text-right">{metrics.histograms['HikariPool-1.pool.Usage'].count}</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(metrics.histograms['HikariPool-1.pool.Usage'].mean)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(metrics.histograms['HikariPool-1.pool.Usage'].min)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(metrics.histograms['HikariPool-1.pool.Usage'].p50)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(metrics.histograms['HikariPool-1.pool.Usage'].p75)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(metrics.histograms['HikariPool-1.pool.Usage'].p95)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(metrics.histograms['HikariPool-1.pool.Usage'].p99)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
<td className="text-right">
<TextFormat
value={this.filterNaN(metrics.histograms['HikariPool-1.pool.Usage'].max)}
type="number"
format={APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT}
/>
</td>
</tr>
</tbody>
</Table>
</Col>
</Row>
) : (
''
)}
{this.renderModal()}
</div>
);
}
}
const mapStateToProps = (storeState: IRootState) => ({
metrics: storeState.administration.metrics,
isFetching: storeState.administration.loading,
threadDump: storeState.administration.threadDump
});
const mapDispatchToProps = { systemMetrics, systemThreadDump };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(MetricsPage);

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { Collapse, Card, CardBody, Row } from 'reactstrap';
export interface IThreadItemProps {
threadDumpInfo: any;
}
export interface IThreadItemState {
collapse: boolean;
}
export class ThreadItem extends React.Component<IThreadItemProps, IThreadItemState> {
state: IThreadItemState = {
collapse: false
};
toggleStackTrace = () => {
this.setState({
collapse: !this.state.collapse
});
};
render() {
const { threadDumpInfo } = this.props;
return (
<div>
<a onClick={this.toggleStackTrace} style={{ color: 'hotpink' }}>
{this.state.collapse ? <span>Hide StackTrace</span> : <span>Show StackTrace</span>}
</a>
<Collapse isOpen={this.state.collapse}>
<Card>
<CardBody>
<Row className="break" style={{ overflowX: 'scroll' }}>
{Object.entries(threadDumpInfo.stackTrace).map(([stK, stV]: [string, any]) => (
<samp key={`detail-${stK}`}>
{stV.className}.{stV.methodName}
<code>
({stV.fileName}:{stV.lineNumber})
</code>
</samp>
))}
<span className="mt-1" />
</Row>
</CardBody>
</Card>
</Collapse>
</div>
);
}
}
export default ThreadItem;

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Switch } from 'react-router-dom';
import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route';
import UserManagement from './user-management';
import UserManagementDetail from './user-management-detail';
import UserManagementUpdate from './user-management-update';
import UserManagementDeleteDialog from './user-management-delete-dialog';
const Routes = ({ match }) => (
<>
<Switch>
<ErrorBoundaryRoute exact path={`${match.url}/new`} component={UserManagementUpdate} />
<ErrorBoundaryRoute exact path={`${match.url}/:login/edit`} component={UserManagementUpdate} />
<ErrorBoundaryRoute exact path={`${match.url}/:login`} component={UserManagementDetail} />
<ErrorBoundaryRoute path={match.url} component={UserManagement} />
</Switch>
<ErrorBoundaryRoute path={`${match.url}/:login/delete`} component={UserManagementDeleteDialog} />
</>
);
export default Routes;

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router-dom';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
import { ICrudGetAction, ICrudDeleteAction } from 'react-jhipster';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IUser } from 'app/shared/model/user.model';
import { getUser, deleteUser } from './user-management.reducer';
import { IRootState } from 'app/shared/reducers';
export interface IUserManagementDeleteDialogProps extends StateProps, DispatchProps, RouteComponentProps<{ login: string }> {}
export class UserManagementDeleteDialog extends React.Component<IUserManagementDeleteDialogProps> {
componentDidMount() {
this.props.getUser(this.props.match.params.login);
}
confirmDelete = event => {
this.props.deleteUser(this.props.user.login);
this.handleClose(event);
};
handleClose = event => {
event.stopPropagation();
this.props.history.goBack();
};
render() {
const { user } = this.props;
return (
<Modal isOpen toggle={this.handleClose}>
<ModalHeader toggle={this.handleClose}>Confirm delete operation</ModalHeader>
<ModalBody>Are you sure you want to delete this User?</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.handleClose}>
<FontAwesomeIcon icon="ban" />&nbsp; Cancel
</Button>
<Button color="danger" onClick={this.confirmDelete}>
<FontAwesomeIcon icon="trash" />&nbsp; Delete
</Button>
</ModalFooter>
</Modal>
);
}
}
const mapStateToProps = (storeState: IRootState) => ({
user: storeState.userManagement.user
});
const mapDispatchToProps = { getUser, deleteUser };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserManagementDeleteDialog);

View File

@@ -0,0 +1,86 @@
import React from 'react';
import { connect } from 'react-redux';
import { Link, RouteComponentProps } from 'react-router-dom';
import { Button, Row, Badge } from 'reactstrap';
import { ICrudGetAction, TextFormat } from 'react-jhipster';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { APP_DATE_FORMAT } from 'app/config/constants';
import { IUser } from 'app/shared/model/user.model';
import { getUser } from './user-management.reducer';
import { IRootState } from 'app/shared/reducers';
export interface IUserManagementDetailProps extends StateProps, DispatchProps, RouteComponentProps<{ login: string }> {}
export class UserManagementDetail extends React.Component<IUserManagementDetailProps> {
componentDidMount() {
this.props.getUser(this.props.match.params.login);
}
render() {
const { user } = this.props;
return (
<div>
<h2>
User [<b>{user.login}</b>]
</h2>
<Row size="md">
<dl className="jh-entity-details">
<dt>Login</dt>
<dd>
<span>{user.login}</span>&nbsp;
{user.activated ? <Badge color="success">Activated</Badge> : <Badge color="danger">Deactivated</Badge>}
</dd>
<dt>First Name</dt>
<dd>{user.firstName}</dd>
<dt>Last Name</dt>
<dd>{user.lastName}</dd>
<dt>Email</dt>
<dd>{user.email}</dd>
<dt>Created By</dt>
<dd>{user.createdBy}</dd>
<dt>Created Date</dt>
<dd>
<TextFormat value={user.createdDate} type="date" format={APP_DATE_FORMAT} blankOnInvalid />
</dd>
<dt>Last Modified By</dt>
<dd>{user.lastModifiedBy}</dd>
<dt>Last Modified Date</dt>
<dd>
<TextFormat value={user.lastModifiedDate} type="date" format={APP_DATE_FORMAT} blankOnInvalid />
</dd>
<dt>Profiles</dt>
<dd>
<ul className="list-unstyled">
{user.authorities
? user.authorities.map((authority, i) => (
<li key={`user-auth-${i}`}>
<Badge color="info">{authority}</Badge>
</li>
))
: null}
</ul>
</dd>
</dl>
</Row>
<Button tag={Link} to="/admin/user-management" replace color="info">
<FontAwesomeIcon icon="arrow-left" /> <span className="d-none d-md-inline">Back</span>
</Button>
</div>
);
}
}
const mapStateToProps = (storeState: IRootState) => ({
user: storeState.userManagement.user
});
const mapDispatchToProps = { getUser };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserManagementDetail);

View File

@@ -0,0 +1,200 @@
import React from 'react';
import { connect } from 'react-redux';
import { Link, RouteComponentProps } from 'react-router-dom';
import { Button, Label, Row, Col } from 'reactstrap';
import { AvForm, AvGroup, AvInput, AvField, AvFeedback } from 'availity-reactstrap-validation';
import { ICrudGetAction, ICrudGetAllAction, ICrudPutAction } from 'react-jhipster';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IUser } from 'app/shared/model/user.model';
import { getUser, getRoles, updateUser, createUser, reset } from './user-management.reducer';
import { IRootState } from 'app/shared/reducers';
export interface IUserManagementUpdateProps extends StateProps, DispatchProps, RouteComponentProps<{ login: string }> {}
export interface IUserManagementUpdateState {
isNew: boolean;
}
export class UserManagementUpdate extends React.Component<IUserManagementUpdateProps, IUserManagementUpdateState> {
state: IUserManagementUpdateState = {
isNew: !this.props.match.params || !this.props.match.params.login
};
componentDidMount() {
!this.state.isNew && this.props.getUser(this.props.match.params.login);
this.props.getRoles();
}
componentWillUnmount() {
this.props.reset();
}
saveUser = (event, values) => {
if (this.state.isNew) {
this.props.createUser(values);
} else {
this.props.updateUser(values);
}
this.handleClose();
};
handleClose = () => {
this.props.history.push('/admin/user-management');
};
render() {
const isInvalid = false;
const { user, loading, updating, roles } = this.props;
const { isNew } = this.state;
return (
<div>
<Row className="justify-content-center">
<Col md="8">
<h1>Create or edit a User</h1>
</Col>
</Row>
<Row className="justify-content-center">
<Col md="8">
{loading ? (
<p>Loading...</p>
) : (
<AvForm onValidSubmit={this.saveUser}>
{user.id ? (
<AvGroup>
<Label for="id">ID</Label>
<AvField type="text" className="form-control" name="id" required readOnly value={user.id} />
</AvGroup>
) : null}
<AvGroup>
<Label for="login">Login</Label>
<AvField
type="text"
className="form-control"
name="login"
validate={{
required: {
value: true,
errorMessage: 'Your username is required.'
},
pattern: {
value: '^[_.@A-Za-z0-9-]*$',
errorMessage: 'Your username can only contain letters and digits.'
},
minLength: {
value: 1,
errorMessage: 'Your username is required to be at least 1 character.'
},
maxLength: {
value: 50,
errorMessage: 'Your username cannot be longer than 50 characters.'
}
}}
value={user.login}
/>
</AvGroup>
<AvGroup>
<Label for="firstName">First Name</Label>
<AvField
type="text"
className="form-control"
name="firstName"
validate={{
maxLength: {
value: 50,
errorMessage: 'This field cannot be longer than {{ max }} characters.'
}
}}
value={user.firstName}
/>
</AvGroup>
<AvGroup>
<Label for="lastName">Last Name</Label>
<AvField
type="text"
className="form-control"
name="lastName"
validate={{
maxLength: {
value: 50,
errorMessage: 'This field cannot be longer than {{ max }} characters.'
}
}}
value={user.lastName}
/>
<AvFeedback>This field cannot be longer than 50 characters.</AvFeedback>
</AvGroup>
<AvGroup>
<AvField
name="email"
label="Email"
placeholder="Your email"
type="email"
validate={{
required: {
value: true,
errorMessage: 'Your email is required.'
},
email: {
errorMessage: 'Your email is invalid.'
},
minLength: {
value: 5,
errorMessage: 'Your email is required to be at least 5 characters.'
},
maxLength: {
value: 254,
errorMessage: 'Your email cannot be longer than 50 characters.'
}
}}
value={user.email}
/>
</AvGroup>
<AvGroup check>
<Label>
<AvInput type="checkbox" name="activated" value={user.activated} /> Activated
</Label>
</AvGroup>
<AvGroup>
<Label for="authorities">Language Key</Label>
<AvInput type="select" className="form-control" name="authorities" value={user.authorities} multiple>
{roles.map(role => (
<option value={role} key={role}>
{role}
</option>
))}
</AvInput>
</AvGroup>
<Button tag={Link} to="/admin/user-management" replace color="info">
<FontAwesomeIcon icon="arrow-left" />&nbsp;
<span className="d-none d-md-inline">Back</span>
</Button>
&nbsp;
<Button color="primary" type="submit" disabled={isInvalid || updating}>
<FontAwesomeIcon icon="save" />&nbsp; Save
</Button>
</AvForm>
)}
</Col>
</Row>
</div>
);
}
}
const mapStateToProps = (storeState: IRootState) => ({
user: storeState.userManagement.user,
roles: storeState.userManagement.authorities,
loading: storeState.userManagement.loading,
updating: storeState.userManagement.updating
});
const mapDispatchToProps = { getUser, getRoles, updateUser, createUser, reset };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserManagementUpdate);

View File

@@ -0,0 +1,164 @@
import axios from 'axios';
import { ICrudGetAction, ICrudGetAllAction, ICrudPutAction, ICrudDeleteAction } from 'react-jhipster';
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
import { IUser, defaultValue } from 'app/shared/model/user.model';
export const ACTION_TYPES = {
FETCH_ROLES: 'userManagement/FETCH_ROLES',
FETCH_USERS: 'userManagement/FETCH_USERS',
FETCH_USER: 'userManagement/FETCH_USER',
CREATE_USER: 'userManagement/CREATE_USER',
UPDATE_USER: 'userManagement/UPDATE_USER',
DELETE_USER: 'userManagement/DELETE_USER',
RESET: 'userManagement/RESET'
};
const initialState = {
loading: false,
errorMessage: null,
users: [] as ReadonlyArray<IUser>,
authorities: [] as any[],
user: defaultValue,
updating: false,
updateSuccess: false,
totalItems: 0
};
export type UserManagementState = Readonly<typeof initialState>;
// Reducer
export default (state: UserManagementState = initialState, action): UserManagementState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.FETCH_ROLES):
return {
...state
};
case REQUEST(ACTION_TYPES.FETCH_USERS):
case REQUEST(ACTION_TYPES.FETCH_USER):
return {
...state,
errorMessage: null,
updateSuccess: false,
loading: true
};
case REQUEST(ACTION_TYPES.CREATE_USER):
case REQUEST(ACTION_TYPES.UPDATE_USER):
case REQUEST(ACTION_TYPES.DELETE_USER):
return {
...state,
errorMessage: null,
updateSuccess: false,
updating: true
};
case FAILURE(ACTION_TYPES.FETCH_USERS):
case FAILURE(ACTION_TYPES.FETCH_USER):
case FAILURE(ACTION_TYPES.FETCH_ROLES):
case FAILURE(ACTION_TYPES.CREATE_USER):
case FAILURE(ACTION_TYPES.UPDATE_USER):
case FAILURE(ACTION_TYPES.DELETE_USER):
return {
...state,
loading: false,
updating: false,
updateSuccess: false,
errorMessage: action.payload
};
case SUCCESS(ACTION_TYPES.FETCH_ROLES):
return {
...state,
loading: false,
authorities: action.payload.data
};
case SUCCESS(ACTION_TYPES.FETCH_USERS):
return {
...state,
loading: false,
users: action.payload.data,
totalItems: action.payload.headers['x-total-count']
};
case SUCCESS(ACTION_TYPES.FETCH_USER):
return {
...state,
loading: false,
user: action.payload.data
};
case SUCCESS(ACTION_TYPES.CREATE_USER):
case SUCCESS(ACTION_TYPES.UPDATE_USER):
return {
...state,
updating: false,
updateSuccess: true,
user: action.payload.data
};
case SUCCESS(ACTION_TYPES.DELETE_USER):
return {
...state,
updating: false,
updateSuccess: true,
user: {}
};
case ACTION_TYPES.RESET:
return {
...state,
user: {}
};
default:
return state;
}
};
const apiUrl = 'api/users';
// Actions
export const getUsers: ICrudGetAllAction<IUser> = (page, size, sort) => {
const requestUrl = `${apiUrl}${sort ? `?page=${page}&size=${size}&sort=${sort}` : ''}`;
return {
type: ACTION_TYPES.FETCH_USERS,
payload: axios.get<IUser>(requestUrl)
};
};
export const getRoles = () => ({
type: ACTION_TYPES.FETCH_ROLES,
payload: axios.get(`${apiUrl}/authorities`)
});
export const getUser: ICrudGetAction<IUser> = id => {
const requestUrl = `${apiUrl}/${id}`;
return {
type: ACTION_TYPES.FETCH_USER,
payload: axios.get<IUser>(requestUrl)
};
};
export const createUser: ICrudPutAction<IUser> = user => async dispatch => {
const result = await dispatch({
type: ACTION_TYPES.CREATE_USER,
payload: axios.post(apiUrl, user)
});
dispatch(getUsers());
return result;
};
export const updateUser: ICrudPutAction<IUser> = user => async dispatch => {
const result = await dispatch({
type: ACTION_TYPES.UPDATE_USER,
payload: axios.put(apiUrl, user)
});
dispatch(getUsers());
return result;
};
export const deleteUser: ICrudDeleteAction<IUser> = id => async dispatch => {
const requestUrl = `${apiUrl}/${id}`;
const result = await dispatch({
type: ACTION_TYPES.DELETE_USER,
payload: axios.delete(requestUrl)
});
dispatch(getUsers());
return result;
};
export const reset = () => ({
type: ACTION_TYPES.RESET
});

View File

@@ -0,0 +1,184 @@
import React from 'react';
import { connect } from 'react-redux';
import { Link, RouteComponentProps } from 'react-router-dom';
import { Button, Table, Row, Badge } from 'reactstrap';
import {
ICrudGetAllAction,
ICrudPutAction,
TextFormat,
JhiPagination,
getPaginationItemsNumber,
getSortState,
IPaginationBaseState
} from 'react-jhipster';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { APP_DATE_FORMAT } from 'app/config/constants';
import { ITEMS_PER_PAGE } from 'app/shared/util/pagination.constants';
import { getUsers, updateUser } from './user-management.reducer';
import { IRootState } from 'app/shared/reducers';
export interface IUserManagementProps extends StateProps, DispatchProps, RouteComponentProps<{}> {}
export class UserManagement extends React.Component<IUserManagementProps, IPaginationBaseState> {
state: IPaginationBaseState = {
...getSortState(this.props.location, ITEMS_PER_PAGE)
};
componentDidMount() {
this.getUsers();
}
sort = prop => () => {
this.setState(
{
order: this.state.order === 'asc' ? 'desc' : 'asc',
sort: prop
},
() => this.sortUsers()
);
};
sortUsers() {
this.getUsers();
this.props.history.push(`${this.props.location.pathname}?page=${this.state.activePage}&sort=${this.state.sort},${this.state.order}`);
}
handlePagination = activePage => this.setState({ activePage }, () => this.sortUsers());
getUsers = () => {
const { activePage, itemsPerPage, sort, order } = this.state;
this.props.getUsers(activePage - 1, itemsPerPage, `${sort},${order}`);
};
toggleActive = user => () => {
this.props.updateUser({
...user,
activated: !user.activated
});
};
render() {
const { users, account, match, totalItems } = this.props;
return (
<div>
<h2 className="userManagement-page-heading">
Users
<Link to={`${match.url}/new`} className="btn btn-primary float-right jh-create-entity">
<FontAwesomeIcon icon="plus" /> Create a new user
</Link>
</h2>
<Table responsive striped>
<thead>
<tr>
<th className="hand" onClick={this.sort('id')}>
ID<FontAwesomeIcon icon="sort" />
</th>
<th className="hand" onClick={this.sort('login')}>
Login<FontAwesomeIcon icon="sort" />
</th>
<th className="hand" onClick={this.sort('email')}>
Email<FontAwesomeIcon icon="sort" />
</th>
<th />
<th>Profiles</th>
<th className="hand" onClick={this.sort('createdDate')}>
Created Date<FontAwesomeIcon icon="sort" />
</th>
<th className="hand" onClick={this.sort('lastModifiedBy')}>
Last Modified By<FontAwesomeIcon icon="sort" />
</th>
<th className="hand" onClick={this.sort('lastModifiedDate')}>
Last Modified Date<FontAwesomeIcon icon="sort" />
</th>
<th />
</tr>
</thead>
<tbody>
{users.map((user, i) => (
<tr id={user.login} key={`user-${i}`}>
<td>
<Button tag={Link} to={`${match.url}/${user.login}`} color="link" size="sm">
{user.id}
</Button>
</td>
<td>{user.login}</td>
<td>{user.email}</td>
<td>
{user.activated ? (
<Button color="success" onClick={this.toggleActive(user)}>
Activated
</Button>
) : (
<Button color="danger" onClick={this.toggleActive(user)}>
Deactivated
</Button>
)}
</td>
<td>
{user.authorities
? user.authorities.map((authority, j) => (
<div key={`user-auth-${i}-${j}`}>
<Badge color="info">{authority}</Badge>
</div>
))
: null}
</td>
<td>
<TextFormat value={user.createdDate} type="date" format={APP_DATE_FORMAT} blankOnInvalid />
</td>
<td>{user.lastModifiedBy}</td>
<td>
<TextFormat value={user.lastModifiedDate} type="date" format={APP_DATE_FORMAT} blankOnInvalid />
</td>
<td className="text-right">
<div className="btn-group flex-btn-group-container">
<Button tag={Link} to={`${match.url}/${user.login}`} color="info" size="sm">
<FontAwesomeIcon icon="eye" /> <span className="d-none d-md-inline">View</span>
</Button>
<Button tag={Link} to={`${match.url}/${user.login}/edit`} color="primary" size="sm">
<FontAwesomeIcon icon="pencil-alt" /> <span className="d-none d-md-inline">Edit</span>
</Button>
<Button
tag={Link}
to={`${match.url}/${user.login}/delete`}
color="danger"
size="sm"
disabled={account.login === user.login}
>
<FontAwesomeIcon icon="trash" /> <span className="d-none d-md-inline">Delete</span>
</Button>
</div>
</td>
</tr>
))}
</tbody>
</Table>
<Row className="justify-content-center">
<JhiPagination
items={getPaginationItemsNumber(totalItems, this.state.itemsPerPage)}
activePage={this.state.activePage}
onSelect={this.handlePagination}
maxButtons={5}
/>
</Row>
</div>
);
}
}
const mapStateToProps = (storeState: IRootState) => ({
users: storeState.userManagement.users,
totalItems: storeState.userManagement.totalItems,
account: storeState.authentication.account
});
const mapDispatchToProps = { getUsers, updateUser };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserManagement);

View File

@@ -0,0 +1,10 @@
/* ==========================================================================
Main page styles
========================================================================== */
.hipster {
display: inline-block;
width: 100%;
height: 497px;
background: url('../../../static/images/logo-jhipster-react.svg') no-repeat center top;
background-size: contain;
}

View File

@@ -0,0 +1,109 @@
import './home.scss';
import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { Row, Col, Alert } from 'reactstrap';
import { IRootState } from 'app/shared/reducers';
import { getSession } from 'app/shared/reducers/authentication';
export interface IHomeProp extends StateProps, DispatchProps {}
export class Home extends React.Component<IHomeProp> {
componentDidMount() {
this.props.getSession();
}
render() {
const { account } = this.props;
return (
<Row>
<Col md="9">
<h2>Welcome, Java Hipster!</h2>
<p className="lead">This is your homepage</p>
{account && account.login ? (
<div>
<Alert color="success">You are logged in as user {account.login}.</Alert>
</div>
) : (
<div>
<Alert color="warning">
If you want to
<Link to="/login" className="alert-link">
{' '}
sign in
</Link>
, you can try the default accounts:
<br />- Administrator (login=&quot;admin&quot; and password=&quot;admin&quot;)
<br />- User (login=&quot;user&quot; and password=&quot;user&quot;).
</Alert>
<Alert color="warning">
You do not have an account yet?&nbsp;
<Link to="/register" className="alert-link">
Register a new account
</Link>
</Alert>
</div>
)}
<p>If you have any question on JHipster:</p>
<ul>
<li>
<a href="https://www.jhipster.tech/" target="_blank" rel="noopener noreferrer">
JHipster homepage
</a>
</li>
<li>
<a href="http://stackoverflow.com/tags/jhipster/info" target="_blank" rel="noopener noreferrer">
JHipster on Stack Overflow
</a>
</li>
<li>
<a href="https://github.com/jhipster/generator-jhipster/issues?state=open" target="_blank" rel="noopener noreferrer">
JHipster bug tracker
</a>
</li>
<li>
<a href="https://gitter.im/jhipster/generator-jhipster" target="_blank" rel="noopener noreferrer">
JHipster public chat room
</a>
</li>
<li>
<a href="https://twitter.com/java_hipster" target="_blank" rel="noopener noreferrer">
follow @java_hipster on Twitter
</a>
</li>
</ul>
<p>
If you like JHipster, do not forget to give us a star on{' '}
<a href="https://github.com/jhipster/generator-jhipster" target="_blank" rel="noopener noreferrer">
Github
</a>!
</p>
</Col>
<Col md="3" className="pad">
<span className="hipster rounded" />
</Col>
</Row>
);
}
}
const mapStateToProps = storeState => ({
account: storeState.authentication.account,
isAuthenticated: storeState.authentication.isAuthenticated
});
const mapDispatchToProps = { getSession };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home);

View File

@@ -0,0 +1,84 @@
import React from 'react';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Label, Alert, Row, Col } from 'reactstrap';
import { AvForm, AvField, AvGroup, AvInput } from 'availity-reactstrap-validation';
import { Link } from 'react-router-dom';
export interface ILoginModalProps {
showModal: boolean;
loginError: boolean;
handleLogin: Function;
handleClose: Function;
}
class LoginModal extends React.Component<ILoginModalProps> {
handleSubmit = (event, errors, { username, password, rememberMe }) => {
const { handleLogin } = this.props;
handleLogin(username, password, rememberMe);
};
render() {
const { loginError, handleClose } = this.props;
return (
<Modal isOpen={this.props.showModal} toggle={handleClose} backdrop="static" id="login-page" autoFocus={false}>
<AvForm onSubmit={this.handleSubmit}>
<ModalHeader id="login-title" toggle={handleClose}>
Sign in
</ModalHeader>
<ModalBody>
<Row>
<Col md="12">
{loginError ? (
<Alert color="danger">
<strong>Failed to sign in!</strong> Please check your credentials and try again.
</Alert>
) : null}
</Col>
<Col md="12">
<AvField
name="username"
label="Username"
placeholder="Your username"
required
errorMessage="Username cannot be empty!"
autoFocus
/>
<AvField
name="password"
type="password"
label="Password"
placeholder="Your password"
required
errorMessage="Password cannot be empty!"
/>
<AvGroup check inline>
<Label className="form-check-label">
<AvInput type="checkbox" name="rememberMe" /> Remember me
</Label>
</AvGroup>
</Col>
</Row>
<div className="mt-1">&nbsp;</div>
<Alert color="warning">
<Link to="/reset/request">Did you forget your password?</Link>
</Alert>
<Alert color="warning">
<span>You don't have an account yet?</span> <Link to="/register">Register a new account</Link>
</Alert>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={handleClose} tabIndex="1">
Cancel
</Button>{' '}
<Button color="primary" type="submit">
Sign in
</Button>
</ModalFooter>
</AvForm>
</Modal>
);
}
}
export default LoginModal;

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { connect } from 'react-redux';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { IRootState } from 'app/shared/reducers';
import { login } from 'app/shared/reducers/authentication';
import LoginModal from './login-modal';
export interface ILoginProps extends StateProps, DispatchProps, RouteComponentProps<{}> {}
export interface ILoginState {
showModal: boolean;
}
export class Login extends React.Component<ILoginProps, ILoginState> {
state: ILoginState = {
showModal: this.props.showModal
};
componentDidUpdate(prevProps: ILoginProps, prevState) {
if (this.props !== prevProps) {
this.setState({ showModal: this.props.showModal });
}
}
handleLogin = (username, password, rememberMe = false) => {
this.props.login(username, password, rememberMe);
};
handleClose = () => {
this.setState({ showModal: false });
};
render() {
const { location, isAuthenticated } = this.props;
const { from } = location.state || { from: { pathname: '/', search: location.search } };
const { showModal } = this.state;
if (isAuthenticated) {
return <Redirect to={from} />;
}
return (
<LoginModal showModal={showModal} handleLogin={this.handleLogin} handleClose={this.handleClose} loginError={this.props.loginError} />
);
}
}
const mapStateToProps = ({ authentication }: IRootState) => ({
isAuthenticated: authentication.isAuthenticated,
loginError: authentication.loginError,
showModal: authentication.showModalLogin
});
const mapDispatchToProps = { login };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(Login);

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { IRootState } from 'app/shared/reducers';
import { logout } from 'app/shared/reducers/authentication';
export interface ILogoutProps extends StateProps, DispatchProps {}
export class Logout extends React.Component<ILogoutProps> {
componentDidMount() {
this.props.logout();
}
render() {
return (
<div className="p-5">
<h4>Logged out successfully!</h4>
<Redirect
to={{
pathname: '/'
}}
/>
</div>
);
}
}
const mapStateToProps = (storeState: IRootState) => ({});
const mapDispatchToProps = { logout };
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(
mapStateToProps,
mapDispatchToProps
)(Logout);

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { Switch } from 'react-router-dom';
import Loadable from 'react-loadable';
import Login from 'app/modules/login/login';
import Register from 'app/modules/account/register/register';
import Activate from 'app/modules/account/activate/activate';
import PasswordResetInit from 'app/modules/account/password-reset/init/password-reset-init';
import PasswordResetFinish from 'app/modules/account/password-reset/finish/password-reset-finish';
import Logout from 'app/modules/login/logout';
import Home from 'app/modules/home/home';
import Entities from 'app/entities';
import PrivateRoute from 'app/shared/auth/private-route';
import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route';
import { AUTHORITIES } from 'app/config/constants';
// tslint:disable:space-in-parens
const Account = Loadable({
loader: () => import(/* webpackChunkName: "account" */ 'app/modules/account'),
loading: () => <div>loading ...</div>
});
const Admin = Loadable({
loader: () => import(/* webpackChunkName: "administration" */ 'app/modules/administration'),
loading: () => <div>loading ...</div>
});
// tslint:enable
const Routes = () => (
<div className="view-routes">
<ErrorBoundaryRoute path="/login" component={Login} />
<Switch>
<ErrorBoundaryRoute path="/logout" component={Logout} />
<ErrorBoundaryRoute path="/register" component={Register} />
<ErrorBoundaryRoute path="/activate/:key?" component={Activate} />
<ErrorBoundaryRoute path="/reset/request" component={PasswordResetInit} />
<ErrorBoundaryRoute path="/reset/finish/:key?" component={PasswordResetFinish} />
<PrivateRoute path="/admin" component={Admin} hasAnyAuthorities={[AUTHORITIES.ADMIN]} />
<PrivateRoute path="/account" component={Account} hasAnyAuthorities={[AUTHORITIES.ADMIN, AUTHORITIES.USER]} />
<PrivateRoute path="/entity" component={Entities} hasAnyAuthorities={[AUTHORITIES.USER]} />
<ErrorBoundaryRoute path="/" component={Home} />
</Switch>
</div>
);
export default Routes;

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { connect } from 'react-redux';
import { Route, Redirect, RouteProps } from 'react-router-dom';
import { IRootState } from 'app/shared/reducers';
import ErrorBoundary from 'app/shared/error/error-boundary';
interface IOwnProps extends RouteProps {
hasAnyAuthorities?: string[];
}
export interface IPrivateRouteProps extends IOwnProps, StateProps {}
export const PrivateRouteComponent = ({
component: Component,
isAuthenticated,
isAuthorized,
hasAnyAuthorities = [],
...rest
}: IPrivateRouteProps) => {
const checkAuthorities = props =>
isAuthorized ? (
<ErrorBoundary>
<Component {...props} />
</ErrorBoundary>
) : (
<div className="insufficient-authority">
<div className="alert alert-danger">You are not authorized to access this page.</div>
</div>
);
const renderRedirect = props =>
isAuthenticated ? (
checkAuthorities(props)
) : (
<Redirect
to={{
pathname: '/login',
search: props.location.search,
state: { from: props.location }
}}
/>
);
if (!Component) throw new Error(`A component needs to be specified for private route for path ${(rest as any).path}`);
return <Route {...rest} render={renderRedirect} />;
};
export const hasAnyAuthority = (authorities: string[], hasAnyAuthorities: string[]) => {
if (authorities && authorities.length !== 0) {
if (hasAnyAuthorities.length === 0) {
return true;
}
return hasAnyAuthorities.some(auth => authorities.includes(auth));
}
return false;
};
const mapStateToProps = ({ authentication: { isAuthenticated, account } }: IRootState, { hasAnyAuthorities = [] }: IOwnProps) => ({
isAuthenticated,
isAuthorized: hasAnyAuthority(account.authorities, hasAnyAuthorities)
});
type StateProps = ReturnType<typeof mapStateToProps>;
/**
* A route wrapped in an authentication check so that routing happens only when you are authenticated.
* Accepts same props as React router Route.
* The route also checks for authorization if hasAnyAuthorities is specified.
*/
export const PrivateRoute = connect<StateProps, undefined, IOwnProps>(
mapStateToProps,
null,
null,
{ pure: false }
)(PrivateRouteComponent);
export default PrivateRoute;

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Route, RouteProps } from 'react-router-dom';
import ErrorBoundary from 'app/shared/error/error-boundary';
export const ErrorBoundaryRoute = ({ component: Component, ...rest }: RouteProps) => {
const encloseInErrorBoundary = props => (
<ErrorBoundary>
<Component {...props} />
</ErrorBoundary>
);
if (!Component) throw new Error(`A component needs to be specified for path ${(rest as any).path}`);
return <Route {...rest} render={encloseInErrorBoundary} />;
};
export default ErrorBoundaryRoute;

View File

@@ -0,0 +1,46 @@
import React from 'react';
interface IErrorBoundaryProps {
readonly children: JSX.Element | JSX.Element[];
}
interface IErrorBoundaryState {
readonly error: any;
readonly errorInfo: any;
}
class ErrorBoundary extends React.Component<IErrorBoundaryProps, IErrorBoundaryState> {
readonly state: IErrorBoundaryState = { error: undefined, errorInfo: undefined };
componentDidCatch(error, errorInfo) {
this.setState({
error,
errorInfo
});
}
render() {
const { error, errorInfo } = this.state;
if (errorInfo) {
const errorDetails =
process.env.NODE_ENV === 'development' ? (
<details className="preserve-space">
{error && error.toString()}
<br />
{errorInfo.componentStack}
</details>
) : (
undefined
);
return (
<div>
<h2 className="error">An unexpected error has occurred.</h2>
{errorDetails}
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -0,0 +1,3 @@
.footer {
height: 50px;
}

View File

@@ -0,0 +1,17 @@
import './footer.scss';
import React from 'react';
import { Col, Row } from 'reactstrap';
const Footer = props => (
<div className="footer page-content">
<Row>
<Col md="12">
<p>Your footer</p>
</Col>
</Row>
</div>
);
export default Footer;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { UncontrolledDropdown, DropdownToggle, DropdownMenu, NavItem, NavLink, NavbarBrand } from 'reactstrap';
import { NavLink as Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import appConfig from 'app/config/constants';
export const NavDropdown = props => (
<UncontrolledDropdown nav inNavbar id={props.id}>
<DropdownToggle nav caret className="d-flex align-items-center">
<FontAwesomeIcon icon={props.icon} />
<span>{props.name}</span>
</DropdownToggle>
<DropdownMenu right style={props.style}>
{props.children}
</DropdownMenu>
</UncontrolledDropdown>
);
export const BrandIcon = props => (
<div {...props} className="brand-icon">
<img src="content/images/logo-jhipster-react.svg" alt="Logo" />
</div>
);
export const Brand = props => (
<NavbarBrand tag={Link} to="/" className="brand-logo">
<BrandIcon />
<span className="brand-title">Payroll</span>
<span className="navbar-version">{appConfig.VERSION}</span>
</NavbarBrand>
);
export const Home = props => (
<NavItem>
<NavLink tag={Link} to="/" className="d-flex align-items-center">
<FontAwesomeIcon icon="home" />
<span>Home</span>
</NavLink>
</NavItem>
);

View File

@@ -0,0 +1,113 @@
$header-color: #fff;
$header-color-secondary: #bbb;
$header-color-hover: darken($header-color, 20%);
/* ==========================================================================
Developement Ribbon
========================================================================== */
.ribbon {
background-color: rgba(170, 0, 0, 0.5);
left: -3.5em;
-moz-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
-o-transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
overflow: hidden;
position: absolute;
top: 30px;
white-space: nowrap;
width: 15em;
z-index: 99999;
pointer-events: none;
opacity: 0.75;
a {
color: #fff;
display: block;
font-weight: 400;
margin: 1px 0;
padding: 10px 50px;
text-align: center;
text-decoration: none;
text-shadow: 0 0 5px #444;
pointer-events: none;
}
}
/* ==========================================================================
Navbar styles
========================================================================== */
.jh-navbar {
background-color: #353d47;
padding: 0.2em 1em;
.profile-image {
margin: -10px 0px;
height: 40px;
width: 40px;
border-radius: 50%;
}
.dropdown-item.active,
.dropdown-item.active:focus,
.dropdown-item.active:hover {
background-color: #353d47;
}
.dropdown-toggle::after {
margin-left: 0.15em;
}
ul.navbar-nav {
padding: 0.5em;
.nav-item {
margin-left: 1.5rem;
}
}
a.nav-link {
font-weight: 400;
> span {
margin-left: 5px;
}
}
.jh-navbar-toggler {
color: #ccc;
font-size: 1.5em;
padding: 10px;
&:hover {
color: #fff;
}
}
}
.navbar-version {
font-size: 10px;
color: $header-color-secondary;
padding: 0 0 0 10px;
}
.brand-logo {
&:hover {
text-decoration: none;
}
.brand-icon {
height: 35px;
width: auto;
display: inline-block;
img {
height: 45px;
}
}
}
.brand-title {
font-size: 24px;
color: $header-color;
&:hover {
color: $header-color-hover;
text-decoration: none;
}
}
.loading-bar {
height: 3px;
background-color: #009cd8;
position: absolute;
top: 0px;
z-index: 1031;
}

View File

@@ -0,0 +1,66 @@
import './header.scss';
import React from 'react';
import { Navbar, Nav, NavbarToggler, NavbarBrand, Collapse } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { NavLink as Link } from 'react-router-dom';
import LoadingBar from 'react-redux-loading-bar';
import { Home, Brand } from './header-components';
import { AdminMenu, EntitiesMenu, AccountMenu } from './menus';
export interface IHeaderProps {
isAuthenticated: boolean;
isAdmin: boolean;
ribbonEnv: string;
isInProduction: boolean;
isSwaggerEnabled: boolean;
}
export interface IHeaderState {
menuOpen: boolean;
}
export default class Header extends React.Component<IHeaderProps, IHeaderState> {
state: IHeaderState = {
menuOpen: false
};
renderDevRibbon = () =>
this.props.isInProduction === false ? (
<div className="ribbon dev">
<a href="">Development</a>
</div>
) : null;
toggleMenu = () => {
this.setState({ menuOpen: !this.state.menuOpen });
};
render() {
const { isAuthenticated, isAdmin, isSwaggerEnabled, isInProduction } = this.props;
/* jhipster-needle-add-element-to-menu - JHipster will add new menu items here */
return (
<div id="app-header">
{this.renderDevRibbon()}
<LoadingBar className="loading-bar" />
<Navbar dark expand="sm" fixed="top" className="jh-navbar">
<NavbarToggler aria-label="Menu" onClick={this.toggleMenu} />
<Brand />
<Collapse isOpen={this.state.menuOpen} navbar>
<Nav id="header-tabs" className="ml-auto" navbar>
<Home />
{isAuthenticated && <EntitiesMenu />}
{isAuthenticated && isAdmin && <AdminMenu showSwagger={isSwaggerEnabled} />}
<AccountMenu isAuthenticated={isAuthenticated} />
</Nav>
</Collapse>
</Navbar>
</div>
);
}
}

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { DropdownItem } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { NavLink as Link } from 'react-router-dom';
import { NavDropdown } from '../header-components';
const accountMenuItemsAuthenticated = (
<>
<DropdownItem tag={Link} to="/account/settings">
<FontAwesomeIcon icon="wrench" /> Settings
</DropdownItem>
<DropdownItem tag={Link} to="/account/password">
<FontAwesomeIcon icon="clock" /> Password
</DropdownItem>
<DropdownItem tag={Link} to="/account/sessions">
<FontAwesomeIcon icon="cloud" /> Sessions
</DropdownItem>
<DropdownItem tag={Link} to="/logout">
<FontAwesomeIcon icon="sign-out-alt" /> Sign out
</DropdownItem>
</>
);
const accountMenuItems = (
<>
<DropdownItem id="login-item" tag={Link} to="/login">
<FontAwesomeIcon icon="sign-in-alt" /> Sign in
</DropdownItem>
<DropdownItem tag={Link} to="/register">
<FontAwesomeIcon icon="sign-in-alt" /> Register
</DropdownItem>
</>
);
export const AccountMenu = ({ isAuthenticated = false }) => (
<NavDropdown icon="user" name="Account" id="account-menu">
{isAuthenticated ? accountMenuItemsAuthenticated : accountMenuItems}
</NavDropdown>
);
export default AccountMenu;

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { DropdownItem } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { NavLink as Link } from 'react-router-dom';
import { NavDropdown } from '../header-components';
const adminMenuItems = (
<>
<DropdownItem tag={Link} to="/admin/user-management">
<FontAwesomeIcon icon="user" /> User management
</DropdownItem>
<DropdownItem tag={Link} to="/admin/metrics">
<FontAwesomeIcon icon="tachometer-alt" /> Metrics
</DropdownItem>
<DropdownItem tag={Link} to="/admin/health">
<FontAwesomeIcon icon="heart" /> Health
</DropdownItem>
<DropdownItem tag={Link} to="/admin/configuration">
<FontAwesomeIcon icon="list" /> Configuration
</DropdownItem>
<DropdownItem tag={Link} to="/admin/audits">
<FontAwesomeIcon icon="bell" /> Audits
</DropdownItem>
{/* jhipster-needle-add-element-to-admin-menu - JHipster will add entities to the admin menu here */}
<DropdownItem tag={Link} to="/admin/logs">
<FontAwesomeIcon icon="tasks" /> Logs
</DropdownItem>
</>
);
const swaggerItem = (
<DropdownItem tag={Link} to="/admin/docs">
<FontAwesomeIcon icon="book" /> API
</DropdownItem>
);
export const AdminMenu = ({ showSwagger }) => (
<NavDropdown icon="user-plus" name="Administration" style={{ width: '140%' }} id="admin-menu">
{adminMenuItems}
{showSwagger && swaggerItem}
</NavDropdown>
);
export default AdminMenu;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { DropdownItem } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { NavLink as Link } from 'react-router-dom';
import { NavDropdown } from '../header-components';
export const EntitiesMenu = props => (
// tslint:disable-next-line:jsx-self-close
<NavDropdown icon="th-list" name="Entities" id="entity-menu">
{/* jhipster-needle-add-entity-to-menu - JHipster will add entities to the menu here */}
</NavDropdown>
);

View File

@@ -0,0 +1,3 @@
export * from './account';
export * from './admin';
export * from './entities';

View File

@@ -0,0 +1,23 @@
/* ==========================================================================
start Password strength bar style
========================================================================== */
ul#strength {
display: inline;
list-style: none;
margin: 0;
margin-left: 15px;
padding: 0;
vertical-align: 2px;
}
.point {
background: #ddd;
border-radius: 2px;
display: inline-block;
height: 5px;
margin-right: 1px;
width: 20px;
&:last {
margin: 0 !important;
}
}

View File

@@ -0,0 +1,73 @@
import './password-strength-bar.scss';
import React from 'react';
export interface IPasswordStrengthBarProps {
password: string;
}
export const PasswordStrengthBar = ({ password }: IPasswordStrengthBarProps) => {
const colors = ['#F00', '#F90', '#FF0', '#9F0', '#0F0'];
const measureStrength = (p: string): number => {
let force = 0;
const regex = /[$-/:-?{-~!"^_`\[\]]/g;
const flags = {
lowerLetters: /[a-z]+/.test(p),
upperLetters: /[A-Z]+/.test(p),
numbers: /[0-9]+/.test(p),
symbols: regex.test(p)
};
const passedMatches = Object.values(flags).filter((isMatchedFlag: boolean) => !!isMatchedFlag).length;
force += 2 * p.length + (p.length >= 10 ? 1 : 0);
force += passedMatches * 10;
// penality (short password)
force = p.length <= 6 ? Math.min(force, 10) : force;
// penality (poor variety of characters)
force = passedMatches === 1 ? Math.min(force, 10) : force;
force = passedMatches === 2 ? Math.min(force, 20) : force;
force = passedMatches === 3 ? Math.min(force, 40) : force;
return force;
};
const getColor = (s: number): any => {
let idx = 0;
if (s <= 10) {
idx = 0;
} else if (s <= 20) {
idx = 1;
} else if (s <= 30) {
idx = 2;
} else if (s <= 40) {
idx = 3;
} else {
idx = 4;
}
return { idx: idx + 1, col: colors[idx] };
};
const getPoints = force => {
const pts = [];
for (let i = 0; i < 5; i++) {
pts.push(<li key={i} className="point" style={i < force.idx ? { backgroundColor: force.col } : { backgroundColor: '#DDD' }} />);
}
return pts;
};
const strength = getColor(measureStrength(password));
const points = getPoints(strength);
return (
<div id="strength">
<small>Password strength:</small>
<ul id="strengthBar">{points}</ul>
</div>
);
};
export default PasswordStrengthBar;

View File

@@ -0,0 +1,31 @@
export interface IUser {
id?: any;
login?: string;
firstName?: string;
lastName?: string;
email?: string;
activated?: boolean;
langKey?: string;
authorities?: any[];
createdBy?: string;
createdDate?: Date;
lastModifiedBy?: string;
lastModifiedDate?: Date;
password?: string;
}
export const defaultValue: Readonly<IUser> = {
id: null,
login: null,
firstName: null,
lastName: null,
email: null,
activated: false,
langKey: null,
authorities: null,
createdBy: null,
createdDate: null,
lastModifiedBy: null,
lastModifiedDate: null,
password: null
};

View File

@@ -0,0 +1,17 @@
/**
* Appends REQUEST asyc action type
*/
export const REQUEST = actionType => `${actionType}_PENDING`;
/**
* Appends SUCCESS asyc action type
*/
export const SUCCESS = actionType => `${actionType}_FULFILLED`;
/**
* Appends FAILURE asyc action type
*/
export const FAILURE = actionType => `${actionType}_REJECTED`;

View File

@@ -0,0 +1,35 @@
import axios from 'axios';
import { SUCCESS } from 'app/shared/reducers/action-type.util';
export const ACTION_TYPES = {
GET_PROFILE: 'applicationProfile/GET_PROFILE'
};
const initialState = {
ribbonEnv: '',
inProduction: true,
isSwaggerEnabled: false
};
export type ApplicationProfileState = Readonly<typeof initialState>;
export default (state: ApplicationProfileState = initialState, action): ApplicationProfileState => {
switch (action.type) {
case SUCCESS(ACTION_TYPES.GET_PROFILE):
const { data } = action.payload;
return {
...state,
ribbonEnv: data['display-ribbon-on-profiles'],
inProduction: data.activeProfiles.includes('prod'),
isSwaggerEnabled: data.activeProfiles.includes('swagger')
};
default:
return state;
}
};
export const getProfile = () => ({
type: ACTION_TYPES.GET_PROFILE,
payload: axios.get('management/info')
});

View File

@@ -0,0 +1,123 @@
import axios from 'axios';
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
export const ACTION_TYPES = {
LOGIN: 'authentication/LOGIN',
GET_SESSION: 'authentication/GET_SESSION',
LOGOUT: 'authentication/LOGOUT',
CLEAR_AUTH: 'authentication/CLEAR_AUTH',
ERROR_MESSAGE: 'authentication/ERROR_MESSAGE'
};
const initialState = {
loading: false,
isAuthenticated: false,
loginSuccess: false,
loginError: false, // Errors returned from server side
showModalLogin: false,
account: {} as any,
errorMessage: null as string, // Errors returned from server side
redirectMessage: null as string
};
export type AuthenticationState = Readonly<typeof initialState>;
// Reducer
export default (state: AuthenticationState = initialState, action): AuthenticationState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.LOGIN):
case REQUEST(ACTION_TYPES.GET_SESSION):
return {
...state,
loading: true
};
case FAILURE(ACTION_TYPES.LOGIN):
return {
...initialState,
errorMessage: action.payload,
showModalLogin: true,
loginError: true
};
case FAILURE(ACTION_TYPES.GET_SESSION):
return {
...state,
loading: false,
isAuthenticated: false,
showModalLogin: true,
errorMessage: action.payload
};
case SUCCESS(ACTION_TYPES.LOGIN):
return {
...state,
loading: false,
loginError: false,
showModalLogin: false,
loginSuccess: true
};
case ACTION_TYPES.LOGOUT:
return {
...initialState,
showModalLogin: true
};
case SUCCESS(ACTION_TYPES.GET_SESSION): {
const isAuthenticated = action.payload && action.payload.data && action.payload.data.activated;
return {
...state,
isAuthenticated,
loading: false,
account: action.payload.data
};
}
case ACTION_TYPES.ERROR_MESSAGE:
return {
...initialState,
showModalLogin: true,
redirectMessage: action.message
};
case ACTION_TYPES.CLEAR_AUTH:
return {
...state,
loading: false,
showModalLogin: true,
isAuthenticated: false
};
default:
return state;
}
};
export const displayAuthError = message => ({ type: ACTION_TYPES.ERROR_MESSAGE, message });
export const getSession = () => dispatch =>
dispatch({
type: ACTION_TYPES.GET_SESSION,
payload: axios.get('api/account')
});
export const login = (username, password, rememberMe = false) => async (dispatch, getState) => {
const data = `j_username=${encodeURIComponent(username)}&j_password=${encodeURIComponent(
password
)}&remember-me=${rememberMe}&submit=Login`;
await dispatch({
type: ACTION_TYPES.LOGIN,
payload: axios.post('api/authentication', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
});
dispatch(getSession());
};
export const logout = () => async dispatch => {
await dispatch({
type: ACTION_TYPES.LOGOUT,
payload: axios.post('api/logout', {})
});
dispatch(getSession());
};
export const clearAuthentication = messageKey => (dispatch, getState) => {
dispatch(displayAuthError(messageKey));
dispatch({
type: ACTION_TYPES.CLEAR_AUTH
});
};

View File

@@ -0,0 +1,47 @@
import { combineReducers } from 'redux';
import { loadingBarReducer as loadingBar } from 'react-redux-loading-bar';
import authentication, { AuthenticationState } from './authentication';
import applicationProfile, { ApplicationProfileState } from './application-profile';
import administration, { AdministrationState } from 'app/modules/administration/administration.reducer';
import userManagement, { UserManagementState } from 'app/modules/administration/user-management/user-management.reducer';
import register, { RegisterState } from 'app/modules/account/register/register.reducer';
import activate, { ActivateState } from 'app/modules/account/activate/activate.reducer';
import password, { PasswordState } from 'app/modules/account/password/password.reducer';
import settings, { SettingsState } from 'app/modules/account/settings/settings.reducer';
import passwordReset, { PasswordResetState } from 'app/modules/account/password-reset/password-reset.reducer';
import sessions, { SessionsState } from 'app/modules/account/sessions/sessions.reducer';
/* jhipster-needle-add-reducer-import - JHipster will add reducer here */
export interface IRootState {
readonly authentication: AuthenticationState;
readonly applicationProfile: ApplicationProfileState;
readonly administration: AdministrationState;
readonly userManagement: UserManagementState;
readonly register: RegisterState;
readonly activate: ActivateState;
readonly passwordReset: PasswordResetState;
readonly password: PasswordState;
readonly settings: SettingsState;
readonly sessions: SessionsState;
/* jhipster-needle-add-reducer-type - JHipster will add reducer type here */
readonly loadingBar: any;
}
const rootReducer = combineReducers<IRootState>({
authentication,
applicationProfile,
administration,
userManagement,
register,
activate,
passwordReset,
password,
settings,
sessions,
/* jhipster-needle-add-reducer-combine - JHipster will add reducer here */
loadingBar
});
export default rootReducer;

View File

@@ -0,0 +1,5 @@
import moment from 'moment';
import { APP_LOCAL_DATETIME_FORMAT } from 'app/config/constants';
export const convertDateTimeFromServer = date => (date ? moment(date).format(APP_LOCAL_DATETIME_FORMAT) : null);

View File

@@ -0,0 +1,26 @@
import pick from 'lodash/pick';
/**
* Removes fields with an 'id' field that equals ''.
* This function was created to prevent entities to be sent to
* the server with relationship fields with empty an empty id and thus
* resulting in a 500.
*
* @param entity Object to clean.
*/
export const cleanEntity = entity => {
const keysToKeep = Object.keys(entity).filter(k => !(entity[k] instanceof Object) || (entity[k]['id'] !== '' && entity[k]['id'] !== -1));
return pick(entity, keysToKeep);
};
/**
* Will return a list of values according to the given keys.
* This function is used to get a values in Many-to-many relations.
*
* @param keyList Keys.
* @param data Array that contains the values.
* @param fieldName Name of the field that contains the key in the value.
*/
export const keysToValues = (keyList: ReadonlyArray<any>, data: ReadonlyArray<any>, fieldName: string) =>
keyList.map((k: any) => data.find((e: any) => e[fieldName] === k));

View File

@@ -0,0 +1 @@
export const ITEMS_PER_PAGE = 20;

View File

@@ -0,0 +1,4 @@
declare module '*.json' {
const value: any;
export default value;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,42 @@
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
<base href="./" />
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>payroll</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="shortcut icon" href="favicon.ico" />
<link rel="manifest" href="manifest.webapp" />
<!-- jhipster-needle-add-resources-to-root - JHipster will add new resources here -->
</head>
<body>
<!--[if lt IE 9]>
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<div id="root"></div>
<noscript>
<h1>You must enable javascript to view this page.</h1>
</noscript>
<!-- uncomment this for adding service worker
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('./service-worker.js')
.then(function() { console.log('Service Worker Registered'); });
}
</script>
-->
<!-- Google Analytics: uncomment and change UA-XXXXX-X to be your site's ID.
<script>
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
e=o.createElement(i);r=o.getElementsByTagName(i)[0];
e.src='//www.google-analytics.com/analytics.js';
r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
ga('create','UA-XXXXX-X');ga('send','pageview');
</script>-->
</body>
</html>

View File

@@ -0,0 +1,31 @@
{
"name": "payroll",
"short_name": "payroll",
"icons": [
{
"src": "./content/images/hipster192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./content/images/hipster256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "./content/images/hipster384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "./content/images/hipster512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#000000",
"background_color": "#e0e0e0",
"start_url": "/index.html",
"display": "standalone",
"orientation": "portrait"
}

View File

@@ -0,0 +1,11 @@
# robotstxt.org/
User-agent: *
Disallow: /api/account
Disallow: /api/account/change-password
Disallow: /api/account/sessions
Disallow: /api/audits/
Disallow: /api/logs/
Disallow: /api/users/
Disallow: /management/
Disallow: /v2/api-docs/

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1,325 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.0"
x="0px"
y="0px"
width="402.035px"
height="509.297px"
viewBox="0 0 402.035 509.297"
enable-background="new 0 0 402.035 509.297"
xml:space="preserve"
id="svg3336"
inkscape:version="0.91 r13725"
sodipodi:docname="logo-jhipster-react.svg"><metadata
id="metadata3377"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs3375" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1536"
inkscape:window-height="801"
id="namedview3373"
showgrid="false"
inkscape:zoom="3.5209766"
inkscape:cx="274.94343"
inkscape:cy="93.655725"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="head"
showguides="false" /><g
id="tie"><path
id="tie_5_"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#3373AA"
d="M212.889,468.099c0,0-87.234-50.614-94.774-19.142 c-7.54,31.472-16.014,55.419,7.217,59.845S212.889,468.099,212.889,468.099z" /><path
id="tie_4_"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4189C6"
d="M197.809,460.493 c25.041-12.944,74.474-35.103,80.132-11.637c7.616,31.586,16.143,55.613-7.032,60.124c-16.286,3.17-52.9-18.183-73.228-31.156 C197.66,462.481,197.831,475.611,197.809,460.493z" /></g><path
style="fill:#53c1de;fill-opacity:1"
d="m 257.81135,507.6133 c -12.32863,-3.35619 -31.56361,-12.5382 -50.20673,-23.96668 l -9.23803,-5.66304 -0.22612,-8.81602 -0.22613,-8.81601 12.27574,-5.62846 c 23.97948,-10.99465 38.92681,-15.62818 50.8345,-15.7582 7.74162,-0.0845 10.50482,0.80615 13.72705,4.42473 2.1905,2.45994 2.99224,4.69904 5.99577,16.74495 6.28312,25.199 6.42466,36.91738 0.52235,43.245 -4.89131,5.24377 -13.8252,6.85615 -23.4584,4.23373 z"
id="path3417"
inkscape:connector-curvature="0" /><path
style="fill:#2c89a0;fill-opacity:1"
d="m 124.02092,508.04228 c -8.91965,-2.40574 -12.64737,-7.12472 -13.39055,-16.95129 -0.51782,-6.84674 0.33189,-12.35107 4.66385,-30.21192 3.31922,-13.68528 4.87721,-17.41096 8.18798,-19.58026 9.19394,-6.0241 32.87383,-0.70518 65.06885,14.61563 l 9.46385,4.50361 -0.22581,8.79049 -0.22581,8.79049 -11.24631,6.74696 c -21.90798,13.14318 -43.75501,22.68202 -54.62491,23.85029 -2.2091,0.23743 -5.66111,-0.0119 -7.67114,-0.554 z"
id="path3423"
inkscape:connector-curvature="0" /><g
id="jacket_2_"><path
id="shoulders_2_"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#495061"
d="M402.025,490.403 c0,0,1.275-34.418-24.249-56.648c-25.523-22.231-87.921-48.58-178.784-47.207c-90.862,1.373,35.346,101.803,35.346,101.803 L402.025,490.403z" /><path
id="shoulders_1_"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#495061"
d="M0.011,490.403c0,0-1.275-34.464,24.255-56.724 c25.53-22.261,87.943-48.645,178.828-47.271c90.885,1.374-35.355,101.94-35.355,101.94s-56.796,0.696-104.288,1.278 C29.329,490.043,0.011,490.403,0.011,490.403z" /></g><g
id="head"><path
id="ear_1_"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#EBCFC2"
d="M88.405,203.989c0,0-11.463-4.608-20.133,0.824 c-8.67,5.432-11.941,29.186-4.52,39.57c7.422,10.383,15.031,22.937,23.009,23.907c7.979,0.969,17.257-1.649,17.257-1.649 l-3.287-31.738L88.405,203.989z" /><path
id="ear"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#EBCFC2"
d="M307.924,203.903c0,0,11.469-4.595,20.144,0.822 c8.675,5.417,11.948,29.106,4.522,39.46c-7.426,10.355-15.039,22.874-23.022,23.841c-7.983,0.967-17.266-1.644-17.266-1.644 l3.289-31.651L307.924,203.903z" /><path
id="earring"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4189C6"
d="M316.968,260.216c4.087,0,7.4,3.313,7.4,7.399 c0,4.086-3.313,7.399-7.4,7.399c-4.087,0-7.4-3.313-7.4-7.399C309.569,263.529,312.882,260.216,316.968,260.216z" /><path
id="face"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#F5E0D4"
d="M192.816,45.238 c69.763-1.072,109.353,34.35,125.797,86.731c20.207,64.371-9.413,137.683-33.71,179.217c-9.957,17.021-18.84,33.648-34.532,44.804 c-10.57,7.515-25.525,12.109-40.699,14.798c-19.135,3.39-45.278-4-55.91-10.276c-37.977-22.419-67.749-96-78.931-146.744 c-5.022-22.79-4.018-55.379,2.056-76.044c11.928-40.586,34.336-69.057,71.942-83.854c9.257-3.643,19.697-5.377,30.421-7.399 C183.772,46.061,188.295,45.649,192.816,45.238z" /><path
id="eye"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#FFFFFF"
d="M146.979,189.927c12.828,0,23.227,10.398,23.227,23.224 c0,12.826-10.399,23.224-23.227,23.224c-12.828,0-23.227-10.398-23.227-23.224C123.751,200.325,134.151,189.927,146.979,189.927z" /><path
id="pupill"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#333333"
d="M156.233,212.945c0,5.45-4.42,9.87-9.87,9.87 c-4.04,0-7.51-2.43-9.03-5.9c0.6,0.35,1.3,0.55,2.04,0.55c2.27,0,4.11-1.84,4.11-4.11s-1.84-4.11-4.11-4.11 c-0.91,0-1.75,0.3-2.44,0.81c1.23-4.04,4.99-6.97,9.43-6.97C151.813,203.085,156.233,207.495,156.233,212.945z" /><path
id="eye_1_"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#FFFFFF"
d="M251.979,189.927 c12.828,0,23.227,10.398,23.227,23.224c0,12.826-10.399,23.224-23.227,23.224c-12.828,0-23.227-10.398-23.227-23.224 C228.751,200.325,239.151,189.927,251.979,189.927z" /><path
id="pupill_1_"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#333333"
d="M261.233,212.945c0,5.45-4.42,9.87-9.87,9.87 c-4.04,0-7.51-2.43-9.03-5.9c0.6,0.35,1.3,0.55,2.04,0.55c2.27,0,4.11-1.84,4.11-4.11s-1.84-4.11-4.11-4.11 c-0.91,0-1.75,0.3-2.44,0.81c1.23-4.04,4.99-6.97,9.43-6.97C256.813,203.085,261.233,207.495,261.233,212.945z" /><path
id="glasses"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4189C6"
d="M187.883,213.768h25.077v10.276h-25.077V213.768z" /><path
id="glasses_2_"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4189C6"
d="M180.483,181.705h-66.6 c-4.54,0-8.22,3.68-8.22,8.22v50.97c0,4.54,3.68,8.22,8.22,8.22h66.6c4.54,0,8.22-3.68,8.22-8.22v-50.97 C188.703,185.385,185.023,181.705,180.483,181.705z M178.763,233.835c0,3.36-2.78,6.09-6.21,6.09h-50.35 c-3.44,0-6.22-2.73-6.22-6.09v-37.74c0-3.36,2.78-6.09,6.22-6.09h50.35c3.43,0,6.21,2.73,6.21,6.09V233.835z" /><path
id="glasses_1_"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4189C6"
d="M284.483,181.705h-66.6 c-4.54,0-8.22,3.68-8.22,8.22v50.97c0,4.54,3.68,8.22,8.22,8.22h66.6c4.54,0,8.22-3.68,8.22-8.22v-50.97 C292.703,185.385,289.023,181.705,284.483,181.705z M282.763,233.835c0,3.36-2.78,6.09-6.21,6.09h-50.35 c-3.44,0-6.22-2.73-6.22-6.09v-37.74c0-3.36,2.78-6.09,6.22-6.09h50.35c3.43,0,6.21,2.73,6.21,6.09V233.835z" /><path
id="eyebrows_1_"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#B08F7E"
d="M148.783,151.061 c-24.698,0.54-39.008,8.574-39.008,16.937c0,8.364,13.151,5.992,22.995,2.066c9.843-3.926,22.872-3.621,28.332,0.413 c5.46,4.034,17.164,5.165,17.656-3.305C179.25,158.702,157.203,150.878,148.783,151.061z" /><path
id="eyebrows"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#B08F7E"
d="M249.548,150.878 c24.728,0.537,39.055,8.531,39.055,16.853c0,8.322-13.166,5.962-23.022,2.055c-9.855-3.906-22.9-3.603-28.366,0.411 c-5.466,4.014-17.184,5.14-17.677-3.288C219.045,158.48,241.117,150.694,249.548,150.878z" /><path
id="neck"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#F5E0D4"
d="M253.253,348.185h-110.59v83.31l55.37,25l55.22-25 V348.185z" /><path
id="beard_1_"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#B08F7E"
d="M311.503,253.395 c-7.69,22.29-17.52,42.26-26.6,57.79c-9.96,17.02-18.84,33.65-34.53,44.81c-10.57,7.51-25.53,12.1-40.7,14.79 c-19.14,3.39-45.28-4-55.91-10.27c-27.23-16.07-50.24-58.45-65.21-99.85c-0.05-2.84,0.14-5.52,0.67-7.85 c0.41-0.13,0.82-0.27,1.23-0.41c6.1-2.35,12.36,4.16,15.62,6.58c11.95,8.86,20.54,20.07,31.24,30 c-2.25-8.18,4.44-26.45,12.75-19.31c3.76,2.73,0.55,12.25,1.64,17.26c18.14,12.21,20-9.94,28.78-14.39 c5.9-1.13,11.45,3.89,17.27,4.94c9.49,1.7,14.05-4.87,22.2-5.35c8.17,4.72,8.23,20.15,22.61,17.27c3.57-0.72,5.17-0.5,6.57-3.29 c2.72-5.18-1.67-13.47,1.24-16.86c2.9-3.38,3.29-0.54,4.93-0.82c5.96,2.76,11.39,11.2,8.22,20.14c3.43-3.7,6.85-7.4,10.28-11.09 c10.42-10.56,19.89-18.84,34.12-25.9c0.82,0.14,1.65,0.27,2.47,0.41C310.763,252.465,311.133,252.925,311.503,253.395z" /><path
id="mounth"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#EBCFC2"
d="M167.739,303.787c0,0,8.352,14.478,31.655,13.976 c23.303-0.503,31.244-13.976,31.244-13.976s-15.235-4.932-27.544-4.932c-5.288,0-10.737,2.929-17.677,4.111 C178.476,304.147,167.739,303.787,167.739,303.787z" /><path
id="spring_boot"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#979A9B"
d="M169.389,370.317 c-5.438-1.343-13.797-0.763-20.371-0.691c-2.418,1.622-4.519,4.226-6.356,7.153c0,5.395,0.389,19.66,0.389,25.564 c1.694,2.451,3.467,4.492,4.93,5.609h20.371c3.173-1.492,4.192-5.293,5.87-8.287c1.84-3.108,3.684-6.215,5.524-9.323 C180.728,386.448,171.329,372.938,169.389,370.317z M157.304,376.187h1.726c0.231,0.345,0.459,0.691,0.691,1.036 c0.114,3.912,0.231,7.827,0.345,11.74c-1.105,0.68-1.088,0.932-2.417,0.691C155.65,388.195,156.21,378.313,157.304,376.187z M160.757,400.011c-13.487,3.308-15.351-11.377-11.74-19.336c0.805-0.691,1.612-1.381,2.417-2.072 c0.691,0.114,1.381,0.231,2.072,0.345c0.114,0.345,0.231,0.691,0.345,1.036c-3.474,5.203-7.758,14.308,2.072,16.919 c11.567,3.073,13.317-11.819,6.906-16.228v-1.381c0.459-0.345,0.922-0.691,1.381-1.036 C173.629,383.141,170.18,397.701,160.757,400.011z" /><path
id="collar"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#FFFFFF"
d="M142.662,389.696l59.609,69.878l-36.588,9.043 l-40.699-45.215c0,0,0.045-13.113,3.289-19.319C131.517,397.877,142.662,389.696,142.662,389.696z" /><path
id="hears"
fill-rule="evenodd"
clip-rule="evenodd"
fill="#B08F7E"
d="M72.364,195.682c0,0,6.133-17.02,6.578-23.841 c0.445-6.821,5.171-46.943,19.733-67.001c9.368-14.438,23.076-17.432,36.588-14.387c13.512,3.046,29.166,11.175,45.221,9.043 c16.055-2.132,56.817-22.612,73.587-14.798c16.77,7.815,44.946,21.469,48.099,47.27c3.152,25.801,21.377,66.59,21.377,66.59 s9.441-65.867,9.455-83.443c0.014-17.576-45.749-69.159-55.087-79.332S238.363-0.89,220.36,0.023 c-18.003,0.914-42.247,15.528-51.799,16.442c-9.552,0.914-15.83-3.592-26.31-1.644c-10.48,1.948-21.592,1.371-53.443,34.528 c-31.85,33.157-28.534,82.852-26.31,110.572C64.722,187.641,72.364,195.682,72.364,195.682z" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#D5AD98"
d="M159.106,87.987c0,0-7.832-11.734,2.056-33.295 c9.887-21.561,10.277-31.651,10.277-31.651s-5.6,13.958-13.155,31.651C150.728,72.385,159.106,87.987,159.106,87.987z"
id="path3364" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#D5AD98"
d="M187.883,91.276c0,0,22.204-25.629,36.588-36.994 c14.384-11.365,32.066-24.252,32.066-24.252s-13.968,3.438-34.943,21.375C200.618,69.341,187.883,91.276,187.883,91.276z"
id="path3366" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#D5AD98"
d="M172.672,87.576c0,0-0.479-23.829,11.1-50.148 C195.35,11.11,218.716,7.011,218.716,7.011s-17.107,7.784-29.188,31.24C177.447,61.706,172.672,87.576,172.672,87.576z"
id="path3368" /><path
style="fill:#53c1de;fill-opacity:1"
d="m 312.3204,272.66636 c -4.51917,-3.88722 -1.70389,-11.75206 4.20673,-11.75206 3.70932,0 5.04431,0.68418 6.38184,3.27068 1.74309,3.37077 1.44397,5.42056 -1.17288,8.03742 -2.92897,2.92897 -6.33974,3.08979 -9.41569,0.44396 z"
id="path3401"
inkscape:connector-curvature="0" /><path
style="fill:#53c1de;fill-opacity:1"
d="m 110.65207,248.05367 c -0.96834,-0.39005 -2.41429,-1.70706 -3.21323,-2.9267 -1.33113,-2.03205 -1.45262,-4.56059 -1.45262,-30.23338 l 0,-28.01586 2.62342,-2.34379 2.62342,-2.34378 36.19796,0 36.19796,0 2.34813,2.34813 2.34813,2.34813 0,13.71802 0,13.71802 10.84465,0 10.84466,0 0,-13.71802 0,-13.71802 2.34813,-2.34813 2.34812,-2.34813 36.19797,0 36.19796,0 2.62342,2.34402 2.62342,2.34403 0,28.01616 c 0,25.63889 -0.12285,28.20366 -1.4478,30.22579 -0.79629,1.21529 -2.35615,2.55497 -3.46635,2.97707 -3.11156,1.18301 -70.39953,1.00588 -73.02913,-0.19225 -3.55375,-1.61919 -4.39574,-4.42822 -4.39574,-14.6649 l 0,-9.27793 -10.84466,0 -10.84465,0 0,9.27793 c 0,10.23668 -0.84199,13.04571 -4.39574,14.6649 -2.52769,1.15169 -70.44524,1.2895 -73.27743,0.14869 z m 66.17427,-9.77201 c 1.85343,-1.74121 1.85921,-1.8134 1.85921,-23.20693 0,-19.92834 -0.10956,-21.59566 -1.53481,-23.35672 l -1.5348,-1.89643 -26.38013,-0.23527 c -14.50908,-0.12941 -27.20268,-0.0298 -28.20801,0.22126 -1.00533,0.25109 -2.54166,1.17031 -3.41406,2.04271 -1.50011,1.50012 -1.58618,2.74931 -1.58618,23.02289 0,20.90396 0.0434,21.4829 1.74664,23.29591 l 1.74665,1.85922 27.72313,0 c 27.3584,0 27.7476,-0.023 29.58236,-1.74664 z m 104.31744,-0.22511 1.97176,-1.97176 0,-20.87771 0,-20.87772 -1.93872,-2.25389 -1.93871,-2.25389 -27.85322,0 -27.85322,0 -1.93871,2.25389 -1.93872,2.25389 0,20.87772 0,20.87771 1.97176,1.97176 1.97175,1.97175 27.78714,0 27.78714,0 1.97175,-1.97175 z"
id="path3403"
inkscape:connector-curvature="0" /><path
style="fill:#53c1de;fill-opacity:1"
d="m 262.22954,508.59222 c -10.39484,-1.68386 -31.94745,-11.49721 -53.21913,-24.23178 l -11.04548,-6.61251 0.0266,-8.60585 0.0266,-8.60585 4.99411,-2.32258 c 31.29069,-14.55212 45.07685,-19.0602 58.53214,-19.14003 7.26131,-0.0431 11.39573,1.61033 14.06981,5.6267 1.72986,2.59819 7.31988,24.03681 9.05317,34.72037 1.0946,6.74682 1.07935,14.02552 -0.0378,18.04957 -2.19624,7.91096 -12.04244,12.79977 -22.39991,11.12196 z"
id="path3405"
inkscape:connector-curvature="0" /><path
style="fill:#53c1de;fill-opacity:1"
d="m 259.26788,507.92831 c -10.80922,-2.34956 -30.45468,-11.57966 -50.85995,-23.89572 l -10.04134,-6.06068 -0.19265,-8.71784 -0.19264,-8.71784 11.7795,-5.51573 c 36.57057,-17.12409 58.97972,-20.64491 65.83723,-10.34403 1.63367,2.45397 7.13614,23.2125 9.0873,34.28254 1.08953,6.18157 1.0645,14.47566 -0.0558,18.51125 -1.25882,4.53427 -5.83949,8.80857 -11.12073,10.37693 -5.11848,1.52002 -7.55726,1.53391 -14.24086,0.0811 l -3e-5,0 z"
id="path3407"
inkscape:connector-curvature="0" /><path
style="fill:#53c1de;fill-opacity:1"
d="m 259.26788,507.92831 c -10.74989,-2.33666 -30.77179,-11.73311 -51.10109,-23.98218 l -9.8002,-5.90494 -0.22571,-8.80014 -0.2257,-8.80015 11.83595,-5.46879 c 27.66619,-12.78314 46.26388,-17.93088 56.84472,-15.7343 7.103,1.47457 9.43656,4.02626 11.9361,13.05182 6.19449,22.36754 8.25225,37.21488 6.2046,44.76797 -1.33962,4.94143 -5.72469,9.15533 -11.22781,10.78959 -5.11848,1.52002 -7.55726,1.53391 -14.24086,0.0811 z"
id="path3409"
inkscape:connector-curvature="0" /><path
style="fill:#53c1de;fill-opacity:1"
d="m 124.98184,508.21104 c -11.5886,-2.074 -16.17419,-11.10774 -13.818,-27.2218 1.38857,-9.49652 7.5837,-34.0401 9.22411,-36.54369 6.55194,-9.99952 31.71528,-5.76564 68.13812,11.46463 l 9.43886,4.46517 0,8.68629 0,8.68629 -11.04548,6.60025 c -22.27381,13.30978 -44.77669,23.22365 -54.82574,24.15403 -1.98818,0.18407 -5.18853,0.053 -7.11187,-0.29117 z"
id="path3413"
inkscape:connector-curvature="0" /><path
style="fill:#53c1de;fill-opacity:1"
d="m 124.98184,508.21104 c -5.26675,-0.94259 -10.29526,-4.24117 -12.22158,-8.01708 -3.60371,-7.06385 -3.01708,-16.13449 2.54079,-39.28648 3.55393,-14.80432 5.10255,-18.02832 9.75598,-20.31051 9.64731,-4.73135 33.44509,1.00642 63.46904,15.30273 l 9.43886,4.49444 0,8.67926 0,8.67926 -12.25044,7.30848 c -15.80472,9.42893 -30.97138,16.84697 -41.572,20.33294 -9.35238,3.07549 -13.94411,3.75056 -19.16065,2.81696 z"
id="path3415"
inkscape:connector-curvature="0" /><g
id="g3395"
transform="matrix(0.18177964,0,0,0.18177964,215.66034,368.26855)"><path
id="path3383"
d="m 207.27414,76.61487 c -2.34402,-0.80681 -4.7729,-1.57019 -7.27378,-2.29214 0.41105,-1.67757 0.78831,-3.33343 1.12375,-4.96113 5.50611,-26.72736 1.90602,-48.25911 -10.38721,-55.34828 -11.78766,-6.7975797 -31.06548,0.28998 -50.53516,17.23384 -1.87224,1.62932 -3.74971,3.35435 -5.62637,5.16224 -1.25044,-1.19615 -2.49886,-2.35207 -3.74407,-3.45771 C 110.42651,14.83421 89.973858,7.1992503 77.692696,14.30893 65.916293,21.12622 62.428821,41.36811 67.385126,66.69784 c 0.478617,2.44658 1.038077,4.94504 1.670737,7.48533 -2.89463,0.82169 -5.689113,1.69769 -8.362935,2.62958 -23.922426,8.34041 -39.200377,21.4119 -39.200377,34.97044 0,14.00339 16.400892,28.049 41.317958,36.56557 1.966353,0.67208 4.006711,1.30755 6.10941,1.91085 -0.682532,2.74743 -1.276179,5.43975 -1.774504,8.06571 -4.725844,24.89012 -1.035262,44.6538 10.709769,51.42805 12.131545,6.99586 32.491686,-0.19507 52.317306,-17.52504 1.56697,-1.36989 3.13957,-2.82264 4.71499,-4.34536 2.04197,1.96635 4.08071,3.82733 6.1086,5.57287 19.20342,16.52517 38.16995,23.19807 49.90412,16.40492 12.11948,-7.01597 16.05822,-28.24688 10.94465,-54.07774 -0.39054,-1.97279 -0.84502,-3.98821 -1.35541,-6.03943 1.42982,-0.42271 2.83349,-0.8591 4.20218,-1.31278 25.90366,-8.58253 42.75743,-22.45721 42.75743,-36.64762 0,-13.60761 -15.77065,-26.76718 -40.17491,-35.16832 l 0,0 0,0 z"
inkscape:connector-curvature="0"
style="fill:#53c1de" /><path
id="path3385"
d="m 201.65582,139.20276 c -1.23556,0.40904 -2.50329,0.8044 -3.79516,1.1877 -2.85964,-9.05311 -6.71914,-18.67976 -11.44217,-28.62575 4.50705,-9.7091 8.21734,-19.212671 10.99694,-28.207061 2.31144,0.66886 4.55491,1.37432 6.71673,2.11879 20.90955,7.19816 33.6641,17.840771 33.6641,26.041211 0,8.73497 -13.77453,20.07418 -36.14044,27.48511 l 0,0 0,0 z m -9.28035,18.38937 c 2.26116,11.42206 2.58413,21.74894 1.08634,29.82189 -1.34576,7.25407 -4.05216,12.09052 -7.39846,14.02751 -7.12095,4.12174 -22.34902,-1.23596 -38.77204,-15.36844 -1.88269,-1.62006 -3.77907,-3.34992 -5.68147,-5.18074 6.36682,-6.96328 12.73002,-15.05835 18.94038,-24.04871 10.92334,-0.9693 21.24338,-2.55397 30.60216,-4.7174 0.46092,1.85937 0.87036,3.68294 1.22309,5.46589 l 0,0 0,0 z m -93.849237,43.13711 c -6.95725,2.45703 -12.49835,2.52742 -15.84787,0.59606 -7.12738,-4.11089 -10.09038,-19.97927 -6.04868,-41.26527 0.46293,-2.43774 1.01394,-4.94183 1.64902,-7.50143 9.25622,2.0468 19.50064,3.51965 30.450117,4.40731 6.2522,8.79731 12.7992,16.88353 19.39688,23.96385 -1.44148,1.39161 -2.87734,2.71645 -4.30514,3.96488 -8.76675,7.66311 -17.55199,13.10004 -25.294327,15.8346 l 0,0 0,0 z m -32.59385,-61.57997 c -11.01786,-3.76579 -20.11682,-8.66016 -26.35372,-14.00097 -5.60425,-4.79944 -8.43373,-9.5643 -8.43373,-13.43065 0,-8.22779 12.26669,-18.722391 32.72537,-25.855401 2.48238,-0.86553 5.08099,-1.68119 7.77774,-2.44738 2.82746,9.1975 6.53654,18.813691 11.01262,28.537671 -4.53399,9.86837 -8.29577,19.63779 -11.15098,28.94308 -1.92251,-0.55262 -3.7851,-1.1342 -5.5773,-1.74635 l 0,0 0,0 z m 10.92575,-74.370721 c -4.24642,-21.70188 -1.4262,-38.07301 5.67061,-42.18108 7.55934,-4.37633 24.275157,1.86339 41.893107,17.50695 1.12615,0.99986 2.25674,2.04639 3.39054,3.12911 -6.56511,7.04935 -13.05218,15.07484 -19.24887,23.82026 -10.626917,0.98499 -20.799347,2.56724 -30.152097,4.68603 -0.58801,-2.36533 -1.10886,-4.68924 -1.55329,-6.96127 l 0,0 0,0 z m 97.467417,24.06722 c -2.23582,-3.86192 -4.53118,-7.63254 -6.87117,-11.3002 7.20943,0.91139 14.1168,2.1212 20.60347,3.6017 -1.94745,6.24133 -4.37472,12.76702 -7.23195,19.456811 -2.0496,-3.891291 -4.21746,-7.814741 -6.50035,-11.758311 l 0,0 0,0 z m -39.74696,-38.71372 c 4.45235,4.82358 8.91113,10.20903 13.29672,16.05219 -4.41937,-0.20874 -8.89344,-0.31734 -13.40451,-0.31734 -4.46844,0 -8.90952,0.10618 -13.30154,0.31131 4.39001,-5.78886 8.8874,-11.16707 13.40933,-16.04616 l 0,0 0,0 z m -40.001557,38.78048 c -2.23341,3.87278 -4.36105,7.77492 -6.37848,11.684311 -2.81098,-6.666461 -5.21573,-13.221911 -7.18007,-19.551731 6.44686,-1.44269 13.32165,-2.62234 20.485227,-3.51643 -2.372977,3.70144 -4.687227,7.49981 -6.926677,11.38305 l 0,8e-4 0,0 z m 7.133007,57.682661 c -7.401277,-0.82572 -14.379437,-1.94463 -20.824687,-3.34751 1.99572,-6.44283 4.45356,-13.13825 7.32406,-19.94829 2.02266,3.90657 4.15874,7.81031 6.40583,11.69154 l 4e-4,0 c 2.28892,3.95402 4.66029,7.8272 7.094397,11.60426 l 0,0 0,0 z m 33.13561,27.38858 c -4.57502,-4.93619 -9.13838,-10.39645 -13.59515,-16.26977 4.32646,0.16972 8.73738,0.2566 13.2203,0.2566 4.60559,0 9.15848,-0.10377 13.63979,-0.30326 -4.40006,5.9791 -8.84276,11.44781 -13.26494,16.31643 l 0,0 0,0 z m 46.07236,-51.03148 c 3.02011,6.88365 5.56604,13.54407 7.58749,19.87711 -6.55143,1.49457 -13.62491,2.69835 -21.07767,3.59285 2.34563,-3.71713 4.66109,-7.55251 6.93634,-11.49768 2.30179,-3.99143 4.48734,-7.98889 6.55384,-11.97228 l 0,0 0,0 z m -14.91517,7.14991 c -3.53212,6.12429 -7.15835,11.97066 -10.83968,17.48924 -6.70507,0.47942 -13.63215,0.72637 -20.69236,0.72637 -7.03165,0 -13.87146,-0.21839 -20.45788,-0.64593 -3.82974,-5.59098 -7.5348,-11.45464 -11.0444,-17.517 l 8e-4,0 c -3.500337,-6.04586 -6.721957,-12.1428 -9.641117,-18.20556 2.91836,-6.07683 6.13153,-12.180611 9.611757,-18.215621 l -8e-4,0.001 c 3.48948,-6.05109 7.16197,-11.8862 10.95632,-17.44219 6.71995,-0.50797 13.61083,-0.77302 20.57492,-0.77302 l 4e-4,0 c 6.99546,0 13.89519,0.26706 20.61313,0.77946 3.73643,5.51536 7.38398,11.33157 10.88714,17.38347 3.54297,6.11986 6.79757,12.183021 9.74087,18.129541 -2.93445,6.04868 -6.18181,12.17297 -9.7091,18.29003 l 0,0 0,0 z m 19.9125,-107.791901 c 7.56658,4.36347 10.50907,21.9613 5.75507,45.0375 -0.30326,1.47245 -0.64473,2.97226 -1.01555,4.49217 -9.37447,-2.16303 -19.55414,-3.77263 -30.21203,-4.7725 -6.20875,-8.84155 -12.64274,-16.87951 -19.10085,-23.83837 1.73629,-1.67033 3.47017,-3.26304 5.19682,-4.76606 16.68162,-14.51739 32.27289,-20.24914 39.37654,-16.15274 l 0,0 0,0 z"
inkscape:connector-curvature="0"
style="fill:#ffffff" /><path
id="path3387"
d="m 134.4708,91.540497 c 11.14334,0 20.17714,9.033403 20.17714,20.177143 0,11.14334 -9.0338,20.17715 -20.17714,20.17715 -11.14334,0 -20.17714,-9.03381 -20.17714,-20.17715 0,-11.14374 9.0338,-20.177143 20.17714,-20.177143"
inkscape:connector-curvature="0"
style="fill:#53c1de" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 246.9508,380.65979 -2.06233,-0.31304 -1.69816,-2.17902 -1.69817,-2.17903 1.77241,-1.34094 c 4.98535,-3.77172 7.44582,-3.10862 7.44523,2.00651 -1.4e-4,1.24983 -0.10018,2.75169 -0.2223,3.33747 -0.2504,1.2011 -0.14991,1.18212 -3.53668,0.66805 z"
id="path3425"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 247.40653,384.37484 c -0.57122,-0.9658 -1.00789,-1.78794 -0.97039,-1.82699 0.0375,-0.039 0.77955,0.0719 1.64897,0.24647 l 1.58077,0.31746 -0.43513,1.3156 c -0.23932,0.72358 -0.514,1.40287 -0.61039,1.50952 -0.0964,0.10666 -0.64262,-0.59627 -1.21383,-1.56206 z"
id="path3427"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 251.51946,393.05629 c -0.0957,-0.27336 -0.53836,-1.37164 -0.98375,-2.44061 l -0.80981,-1.9436 0.92167,-2.5139 c 0.87265,-2.38022 0.95942,-2.50465 1.6317,-2.33973 1.22763,0.30116 3.50442,1.40778 4.70903,2.28879 2.9516,2.15871 1.99151,4.41356 -2.80532,6.58855 -2.19894,0.99704 -2.4298,1.02829 -2.66352,0.3605 z"
id="path3429"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d=""
id="path3431"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 247.11107,393.3754 c 0.4587,-0.76128 0.96599,-1.63355 1.12731,-1.93836 0.26854,-0.5074 0.32879,-0.46932 0.71322,0.45075 0.98996,2.36932 1.01552,2.3184 -1.34119,2.67181 l -1.33333,0.19995 0.83399,-1.38415 z"
id="path3433"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 235.30461,393.1983 c -0.61649,-0.97629 -1.44974,-2.42415 -1.85166,-3.21747 l -0.73076,-1.44239 1.00086,-1.82375 c 0.55047,-1.00306 1.38558,-2.43082 1.85581,-3.1728 l 0.85494,-1.34906 3.66608,0 3.66608,0 0.94106,1.49106 c 0.51759,0.82009 1.36231,2.23678 1.87717,3.14821 1.08187,1.91518 1.13421,1.67213 -1.21728,5.6527 l -1.466,2.48164 -3.7377,0.003 -3.7377,0.003 -1.1209,-1.77508 z m 6.3271,-1.18996 c 2.79116,-1.16622 2.79116,-5.68425 0,-6.85047 -1.29784,-0.54228 -2.08488,-0.51564 -3.25152,0.11003 -2.65862,1.42584 -2.61376,5.23115 0.0774,6.56326 1.40276,0.69436 1.87349,0.72063 3.17415,0.17718 z"
id="path3435"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 239.0862,398.48464 c -0.58632,-0.70144 -1.00437,-1.33701 -0.92899,-1.41239 0.0754,-0.0754 1.04968,-0.0992 2.16511,-0.0529 l 2.02806,0.0841 -1.09907,1.32825 -1.09907,1.32824 -1.06604,-1.27532 z"
id="path3437"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 230.1562,404.04261 c -0.73339,-1.20595 -0.94452,-3.21713 -0.59583,-5.67568 0.16236,-1.14478 0.33746,-2.12369 0.38913,-2.17535 0.0517,-0.0517 0.71966,0.0286 1.48443,0.17838 0.76478,0.14978 1.96563,0.34909 2.66856,0.44292 1.15243,0.15383 1.42605,0.35243 2.78377,2.02061 0.82814,1.0175 1.56725,2.01039 1.64247,2.20641 0.16645,0.43375 -2.67498,2.60121 -4.37878,3.34017 -0.67683,0.29355 -1.7331,0.53373 -2.34728,0.53373 -0.94172,0 -1.19968,-0.13649 -1.64647,-0.87119 z"
id="path3439"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 227.54746,393.40046 c -2.81719,-0.97534 -5.11015,-2.55274 -5.78839,-3.98203 -0.43716,-0.92124 -0.43433,-1.03656 0.0453,-1.85064 0.63677,-1.08066 2.82516,-2.54327 5.02211,-3.35653 0.91873,-0.34009 1.70418,-0.5819 1.74546,-0.53735 0.17026,0.18375 1.83538,4.64754 1.83126,4.90919 -0.002,0.15621 -0.42048,1.3821 -0.92895,2.7242 l -0.92448,2.44018 -1.00236,-0.34702 z"
id="path3441"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 229.6599,379.74125 c -0.37535,-2.2835 -0.23952,-5.26227 0.28633,-6.27916 0.58055,-1.12265 1.49589,-1.39221 3.1525,-0.92839 1.32827,0.37189 4.2292,2.14039 5.11314,3.11713 0.52625,0.58151 0.52156,0.60377 -0.31802,1.50703 -0.46869,0.50424 -1.25386,1.44134 -1.74482,2.08246 -0.81598,1.06553 -1.0337,1.18117 -2.53464,1.34626 -0.9031,0.0993 -2.11117,0.26085 -2.68461,0.35893 l -1.04261,0.17831 -0.22727,-1.38257 z"
id="path3443"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 231.15471,384.86281 c -0.61061,-1.79204 -0.61875,-1.76548 0.60468,-1.97188 0.58578,-0.0988 1.30619,-0.24658 1.60092,-0.32835 0.45672,-0.12672 0.39799,0.0972 -0.39763,1.51586 -1.28978,2.29983 -1.29136,2.30052 -1.80797,0.78437 z"
id="path3445"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 238.27756,379.82061 c 0.32428,-0.54845 1.72854,-2.17197 1.87864,-2.17197 0.0694,0 0.5974,0.57512 1.1734,1.27805 l 1.04727,1.27806 -2.16322,0 c -1.74063,0 -2.11885,-0.075 -1.93609,-0.38414 z"
id="path3447"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 231.61738,394.48391 c -1.07788,-0.13376 -1.133,-0.4764 -0.3767,-2.34196 l 0.51459,-1.26937 0.64664,1.26937 c 0.35564,0.69815 0.83457,1.55692 1.06426,1.90839 0.46003,0.70388 0.41483,0.71448 -1.84879,0.43357 z"
id="path3449"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#979a9b;fill-opacity:1"
d="m 230.7671,406.61798 c -1.19471,-0.27448 -1.68388,-0.70029 -2.42536,-2.1112 -0.52166,-0.99262 -0.59933,-1.5744 -0.55231,-4.13717 0.0301,-1.64017 0.0889,-3.39937 0.13076,-3.90933 0.074,-0.90244 0.0177,-0.95263 -2.10698,-1.87963 -4.32456,-1.88675 -6.50475,-4.2592 -6.06553,-6.60041 0.37863,-2.01828 3.62447,-4.57006 7.34991,-5.77826 l 1.08944,-0.35332 -0.23741,-1.74499 c -0.65278,-4.79818 0.0553,-8.17916 1.91425,-9.14047 1.98011,-1.02395 4.98386,-0.13906 8.39015,2.47172 0.98567,0.75548 1.87301,1.3736 1.97185,1.3736 0.0989,0 0.76093,-0.5116 1.47129,-1.13688 1.85412,-1.63206 4.96532,-3.12331 6.51619,-3.12331 2.94115,0 4.16616,1.74307 4.19164,5.96426 0.009,1.48396 -0.0856,3.29328 -0.21016,4.02071 l -0.22644,1.3226 1.07848,0.3495 c 1.8651,0.60442 4.31764,1.95263 5.50745,3.02755 2.20783,1.99463 2.48694,3.50913 1.03826,5.63367 -1.0409,1.52652 -3.65408,3.2515 -6.08797,4.01872 l -1.49996,0.47282 0.20831,1.15666 c 0.11457,0.63617 0.20831,2.43473 0.20831,3.99679 0,2.32125 -0.10733,3.04473 -0.58751,3.96007 -1.68454,3.21116 -5.05654,2.99959 -9.82278,-0.61632 l -1.88993,-1.4338 -1.29925,1.07363 c -2.92388,2.41616 -5.97747,3.60003 -8.0547,3.12279 z m 4.07826,-2.41726 c 1.07539,-0.54839 2.45815,-1.43091 3.07282,-1.96114 1.26736,-1.09327 1.35856,-0.76631 -1.04542,-3.74819 l -1.3479,-1.67194 -2.77027,-0.45677 c -1.52365,-0.25123 -2.8134,-0.42111 -2.86611,-0.37752 -0.289,0.23901 -0.60151,2.53594 -0.60742,4.46457 -0.0144,4.69073 1.5946,5.77537 5.5643,3.75099 z m 15.3469,-0.0823 c 0.81745,-1.21695 1.09314,-3.53722 0.71944,-6.05502 -0.16412,-1.10581 -0.34145,-2.05361 -0.39406,-2.10622 -0.0526,-0.0526 -1.37118,0.10286 -2.93017,0.34548 l -2.83453,0.44112 -1.42006,1.85106 c -0.78103,1.01808 -1.54786,2.03291 -1.70407,2.25518 -0.37694,0.53634 1.64834,2.32244 3.97617,3.5066 2.2888,1.1643 3.69421,1.09132 4.58728,-0.2382 z m -8.63226,-5.67954 1.06723,-1.33543 -2.30957,-0.0831 c -1.27026,-0.0457 -2.37075,-0.0219 -2.44554,0.0529 -0.19652,0.19652 1.97477,2.73084 2.32577,2.71463 0.16218,-0.007 0.77513,-0.61456 1.36211,-1.34906 z m 3.52351,-5.09859 c 0.6442,-1.0544 1.49509,-2.56556 1.89086,-3.35814 l 0.71959,-1.44105 -1.14442,-2.1091 c -0.62943,-1.16 -1.48775,-2.62032 -1.90738,-3.24515 l -0.76297,-1.13605 -3.8233,0 -3.82331,0 -1.77507,2.98058 c -0.9763,1.63931 -1.77508,3.2246 -1.77508,3.52286 0,0.57767 1.54065,3.49882 2.8987,5.49608 l 0.82075,1.20705 3.75518,0 3.75517,0 1.17128,-1.91708 z m -10.98102,1.46971 c 0,-0.46442 -2.3855,-4.09399 -2.56267,-3.89915 -0.34701,0.38162 -1.19904,3.23558 -1.01955,3.41507 0.36386,0.36387 3.58222,0.79877 3.58222,0.48408 z m 14.12433,-0.15295 c 1.12872,-0.19655 1.77287,-0.43847 1.70152,-0.63902 -0.0631,-0.1775 -0.35283,-0.9957 -0.64372,-1.81823 -0.29089,-0.82252 -0.63551,-1.45996 -0.76582,-1.41652 -0.31438,0.10479 -2.56436,4.19008 -2.3077,4.19008 0.10967,0 1.01674,-0.14234 2.01572,-0.31631 z m -18.48581,-3.47506 1.00698,-2.51331 -1.00218,-2.5989 c -1.16772,-3.02819 -1.11951,-3.01158 -4.332,-1.49208 -4.11901,1.94829 -5.11027,3.98102 -2.9801,6.11119 1.19855,1.19855 4.9549,3.19588 5.78942,3.07836 0.35361,-0.0498 0.82092,-0.84572 1.51788,-2.58526 z m 24.60691,1.68886 c 2.04226,-0.91921 3.78599,-2.25793 4.34229,-3.33371 0.42456,-0.82099 0.42713,-0.98699 0.0282,-1.82345 -0.55101,-1.15547 -1.81968,-2.13072 -4.17437,-3.20892 -3.12941,-1.43293 -2.85322,-1.53626 -4.05803,1.5183 l -1.06257,2.69396 0.53295,1.06908 c 0.29312,0.58799 0.77169,1.74694 1.06349,2.57546 0.60176,1.70864 0.64771,1.71567 3.32799,0.50928 l 10e-6,0 z m -21.25167,-8.58295 c 0.56809,-1.03351 0.99494,-1.91299 0.94856,-1.9544 -0.0464,-0.0414 -0.89517,0.0574 -1.8862,0.21951 -1.73575,0.28399 -1.79636,0.32188 -1.6515,1.03235 0.25447,1.24801 1.04275,2.89901 1.30636,2.73609 0.13744,-0.0849 0.71469,-1.00004 1.28278,-2.03355 z m 16.27896,0.4493 c 0.45589,-1.26706 0.52641,-1.7467 0.26964,-1.834 -0.54561,-0.1855 -3.46337,-0.66847 -3.54642,-0.58703 -0.12736,0.12488 2.2669,4.13682 2.46878,4.13682 0.10485,0 0.46846,-0.7721 0.808,-1.71579 z m -16.57033,-3.82205 c 1.3951,-0.22863 2.62446,-0.51396 2.73191,-0.63407 0.10745,-0.1201 0.93769,-1.10391 1.84498,-2.18624 l 1.64962,-1.96788 -1.1662,-1.03492 c -0.64142,-0.56921 -1.91018,-1.4302 -2.81947,-1.91333 -4.43092,-2.35423 -5.99435,-1.13586 -5.69881,4.44107 0.13964,2.63501 0.35662,3.72641 0.73879,3.71604 0.10045,-0.003 1.32408,-0.19203 2.71918,-0.42067 z m 17.95765,-1.20745 c 0.67824,-4.85336 -0.24821,-7.73933 -2.48447,-7.73933 -1.02555,0 -4.39695,1.75539 -5.86478,3.05363 l -1.20362,1.06455 1.24892,1.4883 c 0.6869,0.81856 1.48597,1.81372 1.77571,2.21147 0.43222,0.59335 0.93397,0.79158 2.79482,1.10419 3.68924,0.61977 3.4719,0.68863 3.73342,-1.18281 z m -8.13968,0.60744 c 0,-0.32717 -2.34421,-2.97384 -2.56075,-2.89114 -0.3367,0.12858 -2.26745,2.51928 -2.26745,2.8076 0,0.14142 1.08634,0.25714 2.4141,0.25714 1.32776,0 2.4141,-0.0781 2.4141,-0.1736 z"
id="path3451"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#979a9b;fill-opacity:1"
d="m 238.76553,391.85809 c -1.1148,-0.45098 -2.10693,-1.94933 -2.10693,-3.18195 0,-3.53721 4.46078,-4.98604 6.37176,-2.06951 0.6419,0.97966 0.63389,2.98551 -0.0159,3.97718 -0.832,1.26978 -2.80064,1.86019 -4.24896,1.27428 z"
id="path3453"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /><path
style="fill:#f5e0d4;fill-opacity:1"
d="m 246.03615,404.32394 c -1.47595,-0.66949 -4.26533,-2.70692 -4.26533,-3.11551 0,-0.15335 0.56372,-0.97093 1.25272,-1.81683 0.68899,-0.8459 1.36663,-1.74155 1.50586,-1.99034 0.28495,-0.50917 5.4539,-1.42083 5.84857,-1.03153 0.12584,0.12413 0.30303,1.37158 0.39377,2.77212 0.17164,2.64936 -0.13763,4.30308 -0.9865,5.27493 -0.58298,0.66743 -2.15904,0.6284 -3.74909,-0.0928 z"
id="path3455"
inkscape:connector-curvature="0"
transform="matrix(5.5011661,0,0,5.5011661,-1186.3834,-2025.9065)" /></g><path
style="fill:#495061;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 254.19084,388.30784 3.69216,6.53228 5.68024,-4.5442 z"
id="path4232"
inkscape:connector-curvature="0" /><path
id="collar_1_"
d="m 253.659,387.696 -59.609,69.878 36.588,9.043 40.699,-45.215 c 0,0 -0.045,-13.113 -3.289,-19.319 -3.244,-6.206 -14.389,-14.387 -14.389,-14.387 z"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd" /><path
style="fill:#53c1de;fill-opacity:1"
d="m 257.71965,507.54949 c -11.7271,-3.07475 -31.52506,-12.5196 -50.12157,-23.91107 l -9.23149,-5.65484 -0.22612,-8.81602 -0.22613,-8.81601 12.27574,-5.62846 c 28.60892,-13.11726 47.66107,-18.13171 57.74858,-15.19919 6.06909,1.76433 8.24174,4.33656 10.62374,12.5776 6.24237,21.59679 8.4615,39.22323 5.81165,46.16175 -1.74464,4.56828 -5.74849,8.0644 -11.03269,9.63364 -5.32762,1.58214 -8.53444,1.51082 -15.62171,-0.3474 z"
id="path3411"
inkscape:connector-curvature="0" /></g><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 253.63473,368.41507 -0.28401,19.59684 8.80438,1.98808 0,-18.17677 z"
id="path3414"
inkscape:connector-curvature="0" /></svg>

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="icon" type="image/png" href="images/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="images/favicon-16x16.png" sizes="16x16" />
<link href='./dist/css/typography.css' media='screen' rel='stylesheet' type='text/css'/>
<link href='./dist/css/reset.css' media='screen' rel='stylesheet' type='text/css'/>
<link href='./dist/css/screen.css' media='screen' rel='stylesheet' type='text/css'/>
<link href='./dist/css/reset.css' media='print' rel='stylesheet' type='text/css'/>
<link href='./dist/css/print.css' media='print' rel='stylesheet' type='text/css'/>
<script src='./dist/lib/object-assign-pollyfill.js' type='text/javascript'></script>
<script src='./dist/lib/jquery-1.8.0.min.js' type='text/javascript'></script>
<script src='./dist/lib/jquery.slideto.min.js' type='text/javascript'></script>
<script src='./dist/lib/jquery.wiggle.min.js' type='text/javascript'></script>
<script src='./dist/lib/jquery.ba-bbq.min.js' type='text/javascript'></script>
<script src='./dist/lib/handlebars-4.0.5.js' type='text/javascript'></script>
<script src='./dist/lib/lodash.min.js' type='text/javascript'></script>
<script src='./dist/lib/backbone-min.js' type='text/javascript'></script>
<script src='./dist/swagger-ui.min.js' type='text/javascript'></script>
<script src='./dist/lib/highlight.9.1.0.pack.js' type='text/javascript'></script>
<script src='./dist/lib/jsoneditor.min.js' type='text/javascript'></script>
<script src='./dist/lib/marked.js' type='text/javascript'></script>
<script src='./dist/lib/swagger-oauth.js' type='text/javascript'></script>
<!-- Some basic translations -->
<!-- <script src='lang/translator.js' type='text/javascript'></script> -->
<!-- <script src='lang/ru.js' type='text/javascript'></script> -->
<!-- <script src='lang/en.js' type='text/javascript'></script> -->
<script type="text/javascript">
$(function() {
var springfox = {
"baseUrl": function() {
var urlMatches = /(.*)\/swagger-ui\/index.html.*/.exec(window.location.href);
return urlMatches[1];
},
"securityConfig": function(cb) {
$.getJSON(this.baseUrl() + "/swagger-resources/configuration/security", function(data) {
cb(data);
});
},
"uiConfig": function(cb) {
$.getJSON(this.baseUrl() + "/swagger-resources/configuration/ui", function(data) {
cb(data);
});
}
};
window.springfox = springfox;
window.oAuthRedirectUrl = springfox.baseUrl() + './dist/o2c.html'
window.springfox.uiConfig(function(data) {
window.swaggerUi = new SwaggerUi({
dom_id: "swagger-ui-container",
validatorUrl: data.validatorUrl,
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
onComplete: function(swaggerApi, swaggerUi) {
initializeSpringfox();
if (window.SwaggerTranslator) {
window.SwaggerTranslator.translate();
}
$('pre code').each(function(i, e) {
hljs.highlightBlock(e)
});
},
onFailure: function(data) {
log("Unable to Load SwaggerUI");
},
docExpansion: "none",
apisSorter: "alpha",
showRequestHeaders: false
});
initializeBaseUrl();
$('#select_baseUrl').change(function() {
window.swaggerUi.headerView.trigger('update-swagger-ui', {
url: $('#select_baseUrl').val()
});
addApiKeyAuthorization();
});
function addApiKeyAuthorization() {
var apiKeyAuth = new SwaggerClient.ApiKeyAuthorization("X-XSRF-TOKEN", getCSRF(), "header");
window.swaggerUi.api.clientAuthorizations.add("key", apiKeyAuth);
}
function getCSRF() {
var name = "XSRF-TOKEN=";
var ca = document.cookie.split(';');
for(var i=0; i<ca.length; i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1);
if (c.indexOf(name) != -1) return c.substring(name.length,c.length);
}
return "";
}
function log() {
if ('console' in window) {
console.log.apply(console, arguments);
}
}
function oAuthIsDefined(security) {
return security.clientId
&& security.clientSecret
&& security.appName
&& security.realm;
}
function initializeSpringfox() {
var security = {};
window.springfox.securityConfig(function(data) {
security = data;
if (typeof initOAuth == "function" && oAuthIsDefined(security)) {
initOAuth(security);
}
});
}
});
function maybePrefix(location, withRelativePath) {
var pat = /^https?:\/\//i;
if (pat.test(location)) {
return location;
}
return withRelativePath + location;
}
function initializeBaseUrl() {
var relativeLocation = springfox.baseUrl();
$('#input_baseUrl').hide();
$.getJSON(relativeLocation + "/swagger-resources", function(data) {
var $urlDropdown = $('#select_baseUrl');
$urlDropdown.empty();
$.each(data, function(i, resource) {
var option = $('<option></option>')
.attr("value", maybePrefix(resource.location, relativeLocation))
.text(resource.name + " (" + resource.location + ")");
$urlDropdown.append(option);
});
$urlDropdown.change();
});
}
});
</script>
</head>
<body class="swagger-section">
<div id='header'>
<div class="swagger-ui-wrap">
<a id="logo" href="http://swagger.io">swagger</a>
<form id='api_selector'>
<div class='input'>
<select id="select_baseUrl" name="select_baseUrl"></select>
</div>
<div class='input'><input placeholder="http://example.com/api" id="input_baseUrl" name="baseUrl" type="text"/>
</div>
</form>
</div>
</div>
<div id="message-bar" class="swagger-ui-wrap" data-sw-translate>&nbsp;</div>
<div id="swagger-ui-container" class="swagger-ui-wrap"></div>
</body>
</html>

View File

@@ -0,0 +1,31 @@
module.exports = {
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
rootDir: '../../../',
coverageDirectory: '<rootDir>/build/test-results/',
testMatch: ['<rootDir>/src/test/javascript/spec/**/+(*.)+(spec.ts?(x))'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'app/(.*)': '<rootDir>/src/main/webapp/app/$1',
'\\.(css|scss)$': 'identity-obj-proxy'
},
reporters: [
'default',
[ 'jest-junit', { output: './build/test-results/jest/TESTS-results.xml' } ]
],
testResultsProcessor: 'jest-sonar-reporter',
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/src/test/javascript/spec/app/modules/account/sessions/sessions.reducer.spec.ts'
],
setupFiles: [
'<rootDir>/src/test/javascript/spec/enzyme-setup.ts',
'<rootDir>/src/test/javascript/spec/storage-mock.ts'
],
globals: {
'ts-jest': {
tsConfigFile: './tsconfig.test.json'
}
}
};

View File

@@ -0,0 +1,32 @@
import axios from 'axios';
import sinon from 'sinon';
import setupAxiosInterceptors from 'app/config/axios-interceptor';
describe('Axios Interceptor', () => {
describe('setupAxiosInterceptors', () => {
const client = axios;
const onUnauthenticated = sinon.spy();
setupAxiosInterceptors(onUnauthenticated);
it('onRequestSuccess is called on fullfilled request', () => {
expect((client.interceptors.request as any).handlers[0].fulfilled({ data: 'foo', url: '/test' })).toMatchObject({
data: 'foo',
timeout: 1000000
});
});
it('onResponseSuccess is called on fullfilled response', () => {
expect((client.interceptors.response as any).handlers[0].fulfilled({ data: 'foo' })).toEqual({ data: 'foo' });
});
it('onResponseError is called on rejected response', () => {
(client.interceptors.response as any).handlers[0].rejected({
response: {
statusText: 'NotFound',
status: 403,
data: { message: 'Page not found' }
}
});
expect(onUnauthenticated.calledOnce).toBe(true);
});
});
});

View File

@@ -0,0 +1,82 @@
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise-middleware';
import * as toastify from 'react-toastify'; // synthetic default import doesn't work here due to mocking.
import sinon from 'sinon';
import notificationMiddleware from 'app/config/notification-middleware';
describe('Notification Middleware', () => {
let store;
const SUCCESS_TYPE = 'SUCCESS';
const ERROR_TYPE = 'SUCCESS';
const DEFAULT_SUCCESS_MESSAGE = 'fooSuccess';
const DEFAULT_ERROR_MESSAGE = 'fooError';
// Default action for use in local tests
const DEFAULT = {
type: SUCCESS_TYPE,
payload: 'foo'
};
const DEFAULT_PROMISE = {
type: SUCCESS_TYPE,
payload: Promise.resolve('foo')
};
const DEFAULT_SUCCESS = {
type: SUCCESS_TYPE,
meta: {
successMessage: DEFAULT_SUCCESS_MESSAGE
},
payload: Promise.resolve('foo')
};
const DEFAULT_ERROR = {
type: ERROR_TYPE,
meta: {
errorMessage: DEFAULT_ERROR_MESSAGE
},
payload: Promise.reject(new Error('foo'))
};
const makeStore = () => applyMiddleware(notificationMiddleware, promiseMiddleware())(createStore)(() => null);
beforeEach(() => {
store = makeStore();
sinon.spy(toastify.toast, 'error');
sinon.spy(toastify.toast, 'success');
});
afterEach(() => {
(toastify.toast as any).error.restore();
(toastify.toast as any).success.restore();
});
it('should not trigger a toast message but should return action', () => {
expect(store.dispatch(DEFAULT).payload).toEqual('foo');
expect((toastify.toast as any).error.called).toEqual(false);
expect((toastify.toast as any).success.called).toEqual(false);
});
it('should not trigger a toast message but should return promise success', async () => {
await store.dispatch(DEFAULT_PROMISE).then(resp => {
expect(resp.value).toEqual('foo');
});
expect((toastify.toast as any).error.called).toEqual(false);
expect((toastify.toast as any).success.called).toEqual(false);
});
it('should trigger a success toast message and return promise success', async () => {
await store.dispatch(DEFAULT_SUCCESS).then(resp => {
expect(resp.value).toEqual('foo');
});
const toastMsg = (toastify.toast as any).success.getCall(0).args[0];
expect(toastMsg).toEqual(DEFAULT_SUCCESS_MESSAGE);
});
it('should trigger an error toast message and return promise error', async () => {
await store.dispatch(DEFAULT_ERROR).catch(err => {
expect(err.message).toEqual('foo');
});
const toastMsg = (toastify.toast as any).error.getCall(0).args[0];
expect(toastMsg).toEqual(DEFAULT_ERROR_MESSAGE);
});
});

View File

@@ -0,0 +1,62 @@
import thunk from 'redux-thunk';
import axios from 'axios';
import sinon from 'sinon';
import configureStore from 'redux-mock-store';
import promiseMiddleware from 'redux-promise-middleware';
import { SUCCESS, FAILURE, REQUEST } from 'app/shared/reducers/action-type.util';
import activate, { ACTION_TYPES, activateAction } from 'app/modules/account/activate/activate.reducer';
describe('Activate reducer tests', () => {
it('should return the initial state', () => {
expect(activate(undefined, {})).toMatchObject({
activationSuccess: false,
activationFailure: false
});
});
it('should reset', () => {
expect(activate({ activationSuccess: true, activationFailure: false }, { type: ACTION_TYPES.RESET })).toMatchObject({
activationSuccess: false,
activationFailure: false
});
});
it('should detect a success', () => {
expect(activate(undefined, { type: SUCCESS(ACTION_TYPES.ACTIVATE_ACCOUNT) })).toMatchObject({
activationSuccess: true,
activationFailure: false
});
});
it('should detect a failure', () => {
expect(activate(undefined, { type: FAILURE(ACTION_TYPES.ACTIVATE_ACCOUNT) })).toMatchObject({
activationSuccess: false,
activationFailure: true
});
});
describe('Actions', () => {
let store;
const resolvedObject = { value: 'whatever' };
beforeEach(() => {
const mockStore = configureStore([thunk, promiseMiddleware()]);
store = mockStore({});
axios.get = sinon.stub().returns(Promise.resolve(resolvedObject));
});
it('dispatches ACTIVATE_ACCOUNT_PENDING and ACTIVATE_ACCOUNT_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.ACTIVATE_ACCOUNT)
},
{
type: SUCCESS(ACTION_TYPES.ACTIVATE_ACCOUNT),
payload: resolvedObject
}
];
await store.dispatch(activateAction('')).then(() => expect(store.getActions()).toEqual(expectedActions));
});
});
});

View File

@@ -0,0 +1,80 @@
import thunk from 'redux-thunk';
import axios from 'axios';
import sinon from 'sinon';
import configureStore from 'redux-mock-store';
import promiseMiddleware from 'redux-promise-middleware';
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
import password, { ACTION_TYPES, savePassword } from 'app/modules/account/password/password.reducer';
describe('Password reducer tests', () => {
describe('Common tests', () => {
it('should return the initial state', () => {
const toTest = password(undefined, {});
expect(toTest).toMatchObject({
loading: false,
errorMessage: null,
updateSuccess: false,
updateFailure: false
});
});
});
describe('Password update', () => {
it('should detect a request', () => {
const toTest = password(undefined, { type: REQUEST(ACTION_TYPES.UPDATE_PASSWORD) });
expect(toTest).toMatchObject({
updateSuccess: false,
updateFailure: false,
loading: true
});
});
it('should detect a success', () => {
const toTest = password(undefined, { type: SUCCESS(ACTION_TYPES.UPDATE_PASSWORD) });
expect(toTest).toMatchObject({
updateSuccess: true,
updateFailure: false,
loading: false
});
});
it('should detect a failure', () => {
const toTest = password(undefined, { type: FAILURE(ACTION_TYPES.UPDATE_PASSWORD) });
expect(toTest).toMatchObject({
updateSuccess: false,
updateFailure: true,
loading: false
});
});
});
describe('Actions', () => {
let store;
const resolvedObject = { value: 'whatever' };
beforeEach(() => {
const mockStore = configureStore([thunk, promiseMiddleware()]);
store = mockStore({});
axios.post = sinon.stub().returns(Promise.resolve(resolvedObject));
});
it('dispatches UPDATE_PASSWORD_PENDING and UPDATE_PASSWORD_FULFILLED actions', async () => {
const meta = {
errorMessage: '<strong>An error has occurred!</strong> The password could not be changed.',
successMessage: '<strong>Password changed!</strong>'
};
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.UPDATE_PASSWORD),
meta
},
{
type: SUCCESS(ACTION_TYPES.UPDATE_PASSWORD),
payload: resolvedObject,
meta
}
];
await store.dispatch(savePassword('', '')).then(() => expect(store.getActions()).toEqual(expectedActions));
});
});
});

View File

@@ -0,0 +1,94 @@
import thunk from 'redux-thunk';
import axios from 'axios';
import sinon from 'sinon';
import configureStore from 'redux-mock-store';
import promiseMiddleware from 'redux-promise-middleware';
import { FAILURE, REQUEST, SUCCESS } from 'app/shared/reducers/action-type.util';
import register, { ACTION_TYPES, handleRegister } from 'app/modules/account/register/register.reducer';
describe('Creating account tests', () => {
const initialState = {
loading: false,
registrationSuccess: false,
registrationFailure: false,
errorMessage: null
};
it('should return the initial state', () => {
expect(register(undefined, {})).toEqual({
...initialState
});
});
it('should detect a request', () => {
expect(register(undefined, { type: REQUEST(ACTION_TYPES.CREATE_ACCOUNT) })).toEqual({
...initialState,
loading: true
});
});
it('should handle RESET', () => {
expect(
register({ loading: true, registrationSuccess: true, registrationFailure: true, errorMessage: '' }, { type: ACTION_TYPES.RESET })
).toEqual({
...initialState
});
});
it('should handle CREATE_ACCOUNT success', () => {
expect(
register(undefined, {
type: SUCCESS(ACTION_TYPES.CREATE_ACCOUNT),
payload: 'fake payload'
})
).toEqual({
...initialState,
registrationSuccess: true
});
});
it('should handle CREATE_ACCOUNT failure', () => {
const payload = { response: { data: { errorKey: 'fake error' } } };
expect(
register(undefined, {
type: FAILURE(ACTION_TYPES.CREATE_ACCOUNT),
payload
})
).toEqual({
...initialState,
registrationFailure: true,
errorMessage: payload.response.data.errorKey
});
});
describe('Actions', () => {
let store;
const resolvedObject = { value: 'whatever' };
beforeEach(() => {
const mockStore = configureStore([thunk, promiseMiddleware()]);
store = mockStore({});
axios.post = sinon.stub().returns(Promise.resolve(resolvedObject));
});
it('dispatches CREATE_ACCOUNT_PENDING and CREATE_ACCOUNT_FULFILLED actions', async () => {
const meta = {
successMessage: '<strong>Registration saved!</strong> Please check your email for confirmation.'
};
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.CREATE_ACCOUNT),
meta
},
{
type: SUCCESS(ACTION_TYPES.CREATE_ACCOUNT),
payload: resolvedObject,
meta
}
];
await store.dispatch(handleRegister('', '', '')).then(() => expect(store.getActions()).toEqual(expectedActions));
});
});
});

View File

@@ -0,0 +1,88 @@
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
import configureStore from 'redux-mock-store';
import promiseMiddleware from 'redux-promise-middleware';
import thunk from 'redux-thunk';
import axios from 'axios';
import sinon from 'sinon';
import account, { ACTION_TYPES, saveAccountSettings } from 'app/modules/account/settings/settings.reducer';
import { ACTION_TYPES as authActionTypes } from 'app/shared/reducers/authentication';
describe('Settings reducer tests', () => {
describe('Common tests', () => {
it('should return the initial state', () => {
const toTest = account(undefined, {});
expect(toTest).toMatchObject({
loading: false,
errorMessage: null,
updateSuccess: false,
updateFailure: false
});
});
});
describe('Settings update', () => {
it('should detect a request', () => {
const toTest = account(undefined, { type: REQUEST(ACTION_TYPES.UPDATE_ACCOUNT) });
expect(toTest).toMatchObject({
updateSuccess: false,
updateFailure: false,
loading: true
});
});
it('should detect a success', () => {
const toTest = account(undefined, { type: SUCCESS(ACTION_TYPES.UPDATE_ACCOUNT) });
expect(toTest).toMatchObject({
updateSuccess: true,
updateFailure: false,
loading: false
});
});
it('should detect a failure', () => {
const toTest = account(undefined, { type: FAILURE(ACTION_TYPES.UPDATE_ACCOUNT) });
expect(toTest).toMatchObject({
updateSuccess: false,
updateFailure: true,
loading: false
});
});
});
describe('Actions', () => {
let store;
const resolvedObject = { value: 'whatever' };
beforeEach(() => {
const mockStore = configureStore([thunk, promiseMiddleware()]);
store = mockStore({});
axios.get = sinon.stub().returns(Promise.resolve(resolvedObject));
axios.post = sinon.stub().returns(Promise.resolve(resolvedObject));
});
it('dispatches UPDATE_ACCOUNT_PENDING and UPDATE_ACCOUNT_FULFILLED actions', async () => {
const meta = {
successMessage: '<strong>Settings saved!</strong>'
};
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.UPDATE_ACCOUNT),
meta
},
{
type: SUCCESS(ACTION_TYPES.UPDATE_ACCOUNT),
payload: resolvedObject,
meta
},
{
type: REQUEST(authActionTypes.GET_SESSION)
},
{
type: SUCCESS(authActionTypes.GET_SESSION),
payload: resolvedObject
}
];
await store.dispatch(saveAccountSettings({})).then(() => expect(store.getActions()).toEqual(expectedActions));
});
});
});

View File

@@ -0,0 +1,303 @@
import configureStore from 'redux-mock-store';
import promiseMiddleware from 'redux-promise-middleware';
import axios from 'axios';
import thunk from 'redux-thunk';
import sinon from 'sinon';
import { REQUEST, FAILURE, SUCCESS } from 'app/shared/reducers/action-type.util';
import administration, {
ACTION_TYPES,
systemHealth,
systemMetrics,
systemThreadDump,
getLoggers,
changeLogLevel,
getConfigurations,
getEnv,
getAudits
} from 'app/modules/administration/administration.reducer';
describe('Administration reducer tests', () => {
function isEmpty(element): boolean {
if (element instanceof Array) {
return element.length === 0;
} else {
return Object.keys(element).length === 0;
}
}
function testInitialState(state) {
expect(state).toMatchObject({
loading: false,
errorMessage: null,
totalItems: 0
});
expect(isEmpty(state.logs.loggers));
expect(isEmpty(state.threadDump));
expect(isEmpty(state.audits));
}
function testMultipleTypes(types, payload, testFunction) {
types.forEach(e => {
testFunction(administration(undefined, { type: e, payload }));
});
}
describe('Common', () => {
it('should return the initial state', () => {
testInitialState(administration(undefined, {}));
});
});
describe('Requests', () => {
it('should set state to loading', () => {
testMultipleTypes(
[
REQUEST(ACTION_TYPES.FETCH_LOGS),
REQUEST(ACTION_TYPES.FETCH_HEALTH),
REQUEST(ACTION_TYPES.FETCH_METRICS),
REQUEST(ACTION_TYPES.FETCH_THREAD_DUMP),
REQUEST(ACTION_TYPES.FETCH_CONFIGURATIONS),
REQUEST(ACTION_TYPES.FETCH_ENV),
REQUEST(ACTION_TYPES.FETCH_AUDITS)
],
{},
state => {
expect(state).toMatchObject({
errorMessage: null,
loading: true
});
}
);
});
});
describe('Failures', () => {
it('should set state to failed and put an error message in errorMessage', () => {
testMultipleTypes(
[
FAILURE(ACTION_TYPES.FETCH_LOGS),
FAILURE(ACTION_TYPES.FETCH_HEALTH),
FAILURE(ACTION_TYPES.FETCH_METRICS),
FAILURE(ACTION_TYPES.FETCH_THREAD_DUMP),
FAILURE(ACTION_TYPES.FETCH_CONFIGURATIONS),
FAILURE(ACTION_TYPES.FETCH_ENV),
FAILURE(ACTION_TYPES.FETCH_AUDITS)
],
'something happened',
state => {
expect(state).toMatchObject({
loading: false,
errorMessage: 'something happened'
});
}
);
});
});
describe('Success', () => {
it('should update state according to a successful fetch logs request', () => {
const payload = { data: [{ name: 'ROOT', level: 'DEBUG' }] };
const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_LOGS), payload });
expect(toTest).toMatchObject({
loading: false,
logs: { loggers: payload.data }
});
});
it('should update state according to a successful fetch health request', () => {
const payload = { data: { status: 'UP' } };
const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_HEALTH), payload });
expect(toTest).toMatchObject({
loading: false,
health: payload.data
});
});
it('should update state according to a successful fetch metrics request', () => {
const payload = { data: { version: '3.1.3', gauges: {} } };
const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_METRICS), payload });
expect(toTest).toMatchObject({
loading: false,
metrics: payload.data
});
});
it('should update state according to a successful fetch thread dump request', () => {
const payload = { data: [{ threadName: 'hz.gateway.cached.thread-6', threadId: 9266 }] };
const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_THREAD_DUMP), payload });
expect(toTest).toMatchObject({
loading: false,
threadDump: payload.data
});
});
it('should update state according to a successful fetch configurations request', () => {
const payload = { data: { contexts: { jhipster: { beans: {} } } } };
const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_CONFIGURATIONS), payload });
expect(toTest).toMatchObject({
loading: false,
configuration: {
configProps: payload.data,
env: {}
}
});
});
it('should update state according to a successful fetch env request', () => {
const payload = { data: { activeProfiles: ['swagger', 'dev'] } };
const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_ENV), payload });
expect(toTest).toMatchObject({
loading: false,
configuration: {
configProps: {},
env: payload.data
}
});
});
it('should update state according to a successful fetch audits request', () => {
const headers = { ['x-total-count']: 1 };
const payload = { data: [{ id: 1, userLogin: 'admin' }], headers };
const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_AUDITS), payload });
expect(toTest).toMatchObject({
loading: false,
audits: payload.data,
totalItems: headers['x-total-count']
});
});
});
describe('Actions', () => {
let store;
const resolvedObject = { value: 'whatever' };
beforeEach(() => {
const mockStore = configureStore([thunk, promiseMiddleware()]);
store = mockStore({});
axios.get = sinon.stub().returns(Promise.resolve(resolvedObject));
axios.put = sinon.stub().returns(Promise.resolve(resolvedObject));
});
it('dispatches FETCH_HEALTH_PENDING and FETCH_HEALTH_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_HEALTH)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_HEALTH),
payload: resolvedObject
}
];
await store.dispatch(systemHealth()).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches FETCH_METRICS_PENDING and FETCH_METRICS_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_METRICS)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_METRICS),
payload: resolvedObject
}
];
await store.dispatch(systemMetrics()).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches FETCH_THREAD_DUMP_PENDING and FETCH_THREAD_DUMP_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_THREAD_DUMP)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_THREAD_DUMP),
payload: resolvedObject
}
];
await store.dispatch(systemThreadDump()).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches FETCH_LOGS_PENDING and FETCH_LOGS_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_LOGS)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_LOGS),
payload: resolvedObject
}
];
await store.dispatch(getLoggers()).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches FETCH_LOGS_CHANGE_LEVEL_PENDING and FETCH_LOGS_CHANGE_LEVEL_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_LOGS_CHANGE_LEVEL)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_LOGS_CHANGE_LEVEL),
payload: resolvedObject
},
{
type: REQUEST(ACTION_TYPES.FETCH_LOGS)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_LOGS),
payload: resolvedObject
}
];
await store.dispatch(changeLogLevel('ROOT', 'DEBUG')).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches FETCH_CONFIGURATIONS_PENDING and FETCH_CONFIGURATIONS_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_CONFIGURATIONS)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_CONFIGURATIONS),
payload: resolvedObject
}
];
await store.dispatch(getConfigurations()).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches FETCH_ENV_PENDING and FETCH_ENV_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_ENV)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_ENV),
payload: resolvedObject
}
];
await store.dispatch(getEnv()).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches FETCH_AUDITS_PENDING and FETCH_AUDITS_FULFILLED actions with pagination variables - no sort', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_AUDITS)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_AUDITS),
payload: resolvedObject
}
];
await store.dispatch(getAudits(1, 10, null, Date.now(), Date.now())).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches FETCH_AUDITS_PENDING and FETCH_AUDITS_FULFILLED actions with pagination variables - no dates', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_AUDITS)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_AUDITS),
payload: resolvedObject
}
];
await store.dispatch(getAudits(1, 10, 'id,desc', null, null)).then(() => expect(store.getActions()).toEqual(expectedActions));
});
});
});

View File

@@ -0,0 +1,279 @@
import configureStore from 'redux-mock-store';
import promiseMiddleware from 'redux-promise-middleware';
import axios from 'axios';
import thunk from 'redux-thunk';
import sinon from 'sinon';
import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
import userManagement, {
ACTION_TYPES,
getUsers,
getRoles,
getUser,
createUser,
updateUser,
deleteUser
} from 'app/modules/administration/user-management/user-management.reducer';
describe('User management reducer tests', () => {
function isEmpty(element): boolean {
if (element instanceof Array) {
return element.length === 0;
} else {
return Object.keys(element).length === 0;
}
}
function testInitialState(state) {
expect(state).toMatchObject({
loading: false,
errorMessage: null,
updating: false,
updateSuccess: false,
totalItems: 0
});
expect(isEmpty(state.users));
expect(isEmpty(state.authorities));
expect(isEmpty(state.user));
}
function testMultipleTypes(types, payload, testFunction) {
types.forEach(e => {
testFunction(userManagement(undefined, { type: e, payload }));
});
}
describe('Common', () => {
it('should return the initial state', () => {
testInitialState(userManagement(undefined, {}));
});
});
describe('Requests', () => {
it('should not modify the current state', () => {
testInitialState(userManagement(undefined, { type: REQUEST(ACTION_TYPES.FETCH_ROLES) }));
});
it('should set state to loading', () => {
testMultipleTypes([REQUEST(ACTION_TYPES.FETCH_USERS), REQUEST(ACTION_TYPES.FETCH_USER)], {}, state => {
expect(state).toMatchObject({
errorMessage: null,
updateSuccess: false,
loading: true
});
});
});
it('should set state to updating', () => {
testMultipleTypes(
[REQUEST(ACTION_TYPES.CREATE_USER), REQUEST(ACTION_TYPES.UPDATE_USER), REQUEST(ACTION_TYPES.DELETE_USER)],
{},
state => {
expect(state).toMatchObject({
errorMessage: null,
updateSuccess: false,
updating: true
});
}
);
});
});
describe('Failures', () => {
it('should set state to failed and put an error message in errorMessage', () => {
testMultipleTypes(
[
FAILURE(ACTION_TYPES.FETCH_USERS),
FAILURE(ACTION_TYPES.FETCH_USER),
FAILURE(ACTION_TYPES.FETCH_ROLES),
FAILURE(ACTION_TYPES.CREATE_USER),
FAILURE(ACTION_TYPES.UPDATE_USER),
FAILURE(ACTION_TYPES.DELETE_USER)
],
'something happened',
state => {
expect(state).toMatchObject({
loading: false,
updating: false,
updateSuccess: false,
errorMessage: 'something happened'
});
}
);
});
});
describe('Success', () => {
it('should update state according to a successful fetch users request', () => {
const headers = { ['x-total-count']: 42 };
const payload = { data: 'some handsome users', headers };
const toTest = userManagement(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_USERS), payload });
expect(toTest).toMatchObject({
loading: false,
users: payload.data,
totalItems: headers['x-total-count']
});
});
it('should update state according to a successful fetch user request', () => {
const payload = { data: 'some handsome user' };
const toTest = userManagement(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_USER), payload });
expect(toTest).toMatchObject({
loading: false,
user: payload.data
});
});
it('should update state according to a successful fetch role request', () => {
const payload = { data: ['ROLE_ADMIN'] };
const toTest = userManagement(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_ROLES), payload });
expect(toTest).toMatchObject({
loading: false,
authorities: payload.data
});
});
it('should set state to successful update', () => {
testMultipleTypes([SUCCESS(ACTION_TYPES.CREATE_USER), SUCCESS(ACTION_TYPES.UPDATE_USER)], { data: 'some handsome user' }, types => {
expect(types).toMatchObject({
updating: false,
updateSuccess: true,
user: 'some handsome user'
});
});
});
it('should set state to successful update with an empty user', () => {
const toTest = userManagement(undefined, { type: SUCCESS(ACTION_TYPES.DELETE_USER) });
expect(toTest).toMatchObject({
updating: false,
updateSuccess: true
});
expect(isEmpty(toTest.user));
});
});
describe('Actions', () => {
let store;
const resolvedObject = { value: 'whatever' };
beforeEach(() => {
const mockStore = configureStore([thunk, promiseMiddleware()]);
store = mockStore({});
axios.get = sinon.stub().returns(Promise.resolve(resolvedObject));
axios.put = sinon.stub().returns(Promise.resolve(resolvedObject));
axios.post = sinon.stub().returns(Promise.resolve(resolvedObject));
axios.delete = sinon.stub().returns(Promise.resolve(resolvedObject));
});
it('dispatches FETCH_USERS_PENDING and FETCH_USERS_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_USERS)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_USERS),
payload: resolvedObject
}
];
await store.dispatch(getUsers()).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches FETCH_USERS_PENDING and FETCH_USERS_FULFILLED actions with pagination options', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_USERS)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_USERS),
payload: resolvedObject
}
];
await store.dispatch(getUsers(1, 20, 'id,desc')).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches FETCH_ROLES_PENDING and FETCH_ROLES_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_ROLES)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_ROLES),
payload: resolvedObject
}
];
await store.dispatch(getRoles()).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches FETCH_USER_PENDING and FETCH_USER_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.FETCH_USER)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_USER),
payload: resolvedObject
}
];
await store.dispatch(getUser('admin')).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches CREATE_USER_PENDING and CREATE_USER_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.CREATE_USER)
},
{
type: SUCCESS(ACTION_TYPES.CREATE_USER),
payload: resolvedObject
},
{
type: REQUEST(ACTION_TYPES.FETCH_USERS)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_USERS),
payload: resolvedObject
}
];
await store.dispatch(createUser()).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches UPDATE_USER_PENDING and UPDATE_USER_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.UPDATE_USER)
},
{
type: SUCCESS(ACTION_TYPES.UPDATE_USER),
payload: resolvedObject
},
{
type: REQUEST(ACTION_TYPES.FETCH_USERS)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_USERS),
payload: resolvedObject
}
];
await store.dispatch(updateUser({ login: 'admin' })).then(() => expect(store.getActions()).toEqual(expectedActions));
});
it('dispatches DELETE_USER_PENDING and DELETE_USER_FULFILLED actions', async () => {
const expectedActions = [
{
type: REQUEST(ACTION_TYPES.DELETE_USER)
},
{
type: SUCCESS(ACTION_TYPES.DELETE_USER),
payload: resolvedObject
},
{
type: REQUEST(ACTION_TYPES.FETCH_USERS)
},
{
type: SUCCESS(ACTION_TYPES.FETCH_USERS),
payload: resolvedObject
}
];
await store.dispatch(deleteUser('admin')).then(() => expect(store.getActions()).toEqual(expectedActions));
});
});
});

View File

@@ -0,0 +1,89 @@
import React from 'react';
import { Route } from 'react-router-dom';
import { shallow } from 'enzyme';
import { PrivateRouteComponent, hasAnyAuthority } from 'app/shared/auth/private-route';
const TestComp = () => <div>Test</div>;
describe('private-route component', () => {
// All tests will go here
it('Should throw error when no component is provided', () => {
expect(() => shallow(<PrivateRouteComponent component={null} isAuthenticated isAuthorized />)).toThrow(Error);
});
it('Should render an error message when the user has no authorities', () => {
const route = shallow(<PrivateRouteComponent component={TestComp} isAuthenticated isAuthorized={false} path="/" />);
const renderedRoute = route.find(Route);
const renderFn: Function = renderedRoute.props().render;
const comp = shallow(
renderFn({
location: '/'
})
);
expect(comp.length).toEqual(1);
const error = comp.find('div.insufficient-authority');
expect(error.length).toEqual(1);
expect(error.find('.alert-danger').html()).toEqual('<div class="alert alert-danger">You are not authorized to access this page.</div>');
});
it('Should render a route for the component provided when authenticated', () => {
const route = shallow(<PrivateRouteComponent component={TestComp} isAuthenticated isAuthorized path="/" />);
const renderedRoute = route.find(Route);
expect(renderedRoute.length).toEqual(1);
expect(renderedRoute.props().path).toEqual('/');
// tslint:disable-next-line:no-unused-expression
expect(renderedRoute.props().render).toBeDefined();
const renderFn: Function = renderedRoute.props().render;
const comp = shallow(
renderFn({
location: '/'
})
);
expect(comp.length).toEqual(1);
expect(comp.html()).toEqual('<div>Test</div>');
});
it('Should render a redirect to login when not authenticated', () => {
const route = shallow(<PrivateRouteComponent component={TestComp} isAuthenticated={false} isAuthorized path="/" />);
const renderedRoute = route.find(Route);
expect(renderedRoute.length).toEqual(1);
const renderFn: Function = renderedRoute.props().render;
// as rendering redirect outside router will throw error
expect(() =>
shallow(
renderFn({
location: '/'
})
)
).toThrow(Error);
});
});
describe('hasAnyAuthority', () => {
// All tests will go here
it('Should return false when authorities is invlaid', () => {
expect(hasAnyAuthority(undefined, undefined)).toEqual(false);
expect(hasAnyAuthority(null, [])).toEqual(false);
expect(hasAnyAuthority([], [])).toEqual(false);
expect(hasAnyAuthority([], ['ROLE_USER'])).toEqual(false);
});
it('Should return true when authorities is valid and hasAnyAuthorities is empty', () => {
expect(hasAnyAuthority(['ROLE_USER'], [])).toEqual(true);
});
it('Should return true when authorities is valid and hasAnyAuthorities contains an authority', () => {
expect(hasAnyAuthority(['ROLE_USER'], ['ROLE_USER'])).toEqual(true);
expect(hasAnyAuthority(['ROLE_USER', 'ROLE_ADMIN'], ['ROLE_USER'])).toEqual(true);
expect(hasAnyAuthority(['ROLE_USER', 'ROLE_ADMIN'], ['ROLE_USER', 'ROLE_ADMIN'])).toEqual(true);
expect(hasAnyAuthority(['ROLE_USER', 'ROLE_ADMIN'], ['ROLE_USER', 'ROLEADMIN'])).toEqual(true);
expect(hasAnyAuthority(['ROLE_USER', 'ROLE_ADMIN'], ['ROLE_ADMIN'])).toEqual(true);
});
it('Should return false when authorities is valid and hasAnyAuthorities does not contains an authority', () => {
expect(hasAnyAuthority(['ROLE_USER'], ['ROLE_ADMIN'])).toEqual(false);
expect(hasAnyAuthority(['ROLE_USER', 'ROLE_ADMIN'], ['ROLE_USERSS'])).toEqual(false);
expect(hasAnyAuthority(['ROLE_USER', 'ROLE_ADMIN'], ['ROLEUSER', 'ROLEADMIN'])).toEqual(false);
});
});

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