[add] Jhipster base
61
front-end/src/main/webapp/404.html
Normal 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 -->
|
||||
28
front-end/src/main/webapp/app/_bootstrap-variables.scss
Normal 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;
|
||||
344
front-end/src/main/webapp/app/app.scss
Normal 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 */
|
||||
73
front-end/src/main/webapp/app/app.tsx
Normal 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);
|
||||
25
front-end/src/main/webapp/app/config/axios-interceptor.ts
Normal 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;
|
||||
23
front-end/src/main/webapp/app/config/constants.ts
Normal 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]';
|
||||
11
front-end/src/main/webapp/app/config/devtools.tsx
Normal 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>
|
||||
);
|
||||
38
front-end/src/main/webapp/app/config/error-middleware.ts
Normal 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);
|
||||
};
|
||||
63
front-end/src/main/webapp/app/config/icon-loader.ts
Normal 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
|
||||
);
|
||||
};
|
||||
14
front-end/src/main/webapp/app/config/logger-middleware.ts
Normal 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);
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
29
front-end/src/main/webapp/app/config/store.ts
Normal 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;
|
||||
18
front-end/src/main/webapp/app/entities/index.tsx
Normal 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;
|
||||
50
front-end/src/main/webapp/app/index.tsx
Normal 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);
|
||||
// });
|
||||
// }
|
||||
@@ -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
|
||||
});
|
||||
@@ -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);
|
||||
17
front-end/src/main/webapp/app/modules/account/index.tsx
Normal 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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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
|
||||
});
|
||||
@@ -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> </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);
|
||||
@@ -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}`)
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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" /> 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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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
|
||||
<Badge pill>{counters.threadDumpAll || 0}</Badge>
|
||||
</Badge>
|
||||
<Badge color="success" className="hand" onClick={this.updateBadgeFilter('RUNNABLE')}>
|
||||
Runnable
|
||||
<Badge pill>{counters.threadDumpRunnable || 0}</Badge>
|
||||
</Badge>
|
||||
<Badge color="info" className="hand" onClick={this.updateBadgeFilter('WAITING')}>
|
||||
Waiting
|
||||
<Badge pill>{counters.threadDumpWaiting || 0}</Badge>
|
||||
</Badge>
|
||||
<Badge color="warning" className="hand" onClick={this.updateBadgeFilter('TIMED_WAITING')}>
|
||||
Timed Waiting
|
||||
<Badge pill>{counters.threadDumpTimedWaiting || 0}</Badge>
|
||||
</Badge>
|
||||
<Badge color="danger" className="hand" onClick={this.updateBadgeFilter('BLOCKED')}>
|
||||
Blocked
|
||||
<Badge pill>{counters.threadDumpBlocked || 0}</Badge>
|
||||
</Badge>
|
||||
<div className="mt-2"> </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>
|
||||
{threadDumpInfo.threadName} (ID {threadDumpInfo.threadId})
|
||||
</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;
|
||||
@@ -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" /> 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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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" /> Cancel
|
||||
</Button>
|
||||
<Button color="danger" onClick={this.confirmDelete}>
|
||||
<FontAwesomeIcon icon="trash" /> 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);
|
||||
@@ -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>
|
||||
{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);
|
||||
@@ -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" />
|
||||
<span className="d-none d-md-inline">Back</span>
|
||||
</Button>
|
||||
|
||||
<Button color="primary" type="submit" disabled={isInvalid || updating}>
|
||||
<FontAwesomeIcon icon="save" /> 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);
|
||||
@@ -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
|
||||
});
|
||||
@@ -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);
|
||||
10
front-end/src/main/webapp/app/modules/home/home.scss
Normal 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;
|
||||
}
|
||||
109
front-end/src/main/webapp/app/modules/home/home.tsx
Normal 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="admin" and password="admin")
|
||||
<br />- User (login="user" and password="user").
|
||||
</Alert>
|
||||
|
||||
<Alert color="warning">
|
||||
You do not have an account yet?
|
||||
<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);
|
||||
84
front-end/src/main/webapp/app/modules/login/login-modal.tsx
Normal 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"> </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;
|
||||
61
front-end/src/main/webapp/app/modules/login/login.tsx
Normal 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);
|
||||
39
front-end/src/main/webapp/app/modules/login/logout.tsx
Normal 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);
|
||||
46
front-end/src/main/webapp/app/routes.tsx
Normal 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;
|
||||
79
front-end/src/main/webapp/app/shared/auth/private-route.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
113
front-end/src/main/webapp/app/shared/layout/header/header.scss
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './account';
|
||||
export * from './admin';
|
||||
export * from './entities';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
31
front-end/src/main/webapp/app/shared/model/user.model.ts
Normal 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
|
||||
};
|
||||
@@ -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`;
|
||||
@@ -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')
|
||||
});
|
||||
123
front-end/src/main/webapp/app/shared/reducers/authentication.ts
Normal 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
|
||||
});
|
||||
};
|
||||
47
front-end/src/main/webapp/app/shared/reducers/index.ts
Normal 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;
|
||||
5
front-end/src/main/webapp/app/shared/util/date-utils.ts
Normal 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);
|
||||
26
front-end/src/main/webapp/app/shared/util/entity-utils.ts
Normal 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));
|
||||
@@ -0,0 +1 @@
|
||||
export const ITEMS_PER_PAGE = 20;
|
||||
4
front-end/src/main/webapp/app/typings.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.json' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
BIN
front-end/src/main/webapp/favicon.ico
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
42
front-end/src/main/webapp/index.html
Normal 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>
|
||||
31
front-end/src/main/webapp/manifest.webapp
Normal 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"
|
||||
}
|
||||
11
front-end/src/main/webapp/robots.txt
Normal 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/
|
||||
BIN
front-end/src/main/webapp/static/images/hipster.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
front-end/src/main/webapp/static/images/hipster192.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
front-end/src/main/webapp/static/images/hipster256.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
front-end/src/main/webapp/static/images/hipster2x.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
front-end/src/main/webapp/static/images/hipster384.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
front-end/src/main/webapp/static/images/hipster512.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
325
front-end/src/main/webapp/static/images/logo-jhipster-react.svg
Normal 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 |
BIN
front-end/src/main/webapp/static/images/logo-jhipster.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
front-end/src/main/webapp/swagger-ui/dist/images/throbber.gif
vendored
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
175
front-end/src/main/webapp/swagger-ui/index.html
Normal 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> </div>
|
||||
<div id="swagger-ui-container" class="swagger-ui-wrap"></div>
|
||||
</body>
|
||||
</html>
|
||||
31
front-end/src/test/javascript/jest.conf.js
Normal 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'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||