Client - add application configuration in Application Admin - #15

This commit is contained in:
Sam
2019-11-13 20:15:50 +01:00
parent 97534b698b
commit 1398c7ff4a
28 changed files with 563 additions and 97 deletions

View File

@ -5,17 +5,27 @@ import { connect } from 'react-redux'
import { setLoading } from '../../../actions/index'
import { addActivity, editActivity } from '../../../actions/activities'
import { history } from '../../../index'
import { fileSizeLimit, gpxLimit, zipSizeLimit } from '../../../utils'
import { getFileSize } from '../../../utils'
import { translateSports } from '../../../utils/activities'
function FormWithGpx(props) {
const { activity, loading, onAddActivity, onEditActivity, sports, t } = props
const {
activity,
appConfig,
loading,
onAddActivity,
onEditActivity,
sports,
t,
} = props
const sportId = activity ? activity.sport_id : ''
const translatedSports = translateSports(sports, t, true)
// prettier-ignore
const zipTooltip =
`${t('activities:no folder inside')}, ${gpxLimit} ${
t('activities:files max')}, ${t('activities:max size')}: ${zipSizeLimit}`
const zipTooltip = `${t('activities:no folder inside')}, ${
appConfig.gpx_limit_import
} ${t('activities:files max')}, ${t('activities:max size')}: ${getFileSize(
appConfig.max_zip_file_size
)}`
const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
return (
<form
encType="multipart/form-data"
@ -130,6 +140,7 @@ function FormWithGpx(props) {
export default connect(
state => ({
appConfig: state.application.config,
loading: state.loading,
}),
dispatch => ({

View File

@ -3,15 +3,16 @@ import { Helmet } from 'react-helmet'
import AdminStats from './AdminStats'
export default function AdminDashboard() {
export default function AdminDashboard(props) {
const { t } = props
return (
<div>
<Helmet>
<title>FitTrackee - Administration</title>
<title>{t('administration:FitTrackee administration')}</title>
</Helmet>
<div className="card activity-card">
<div className="card-header">
<strong>FitTrackee administration</strong>
<strong>{t('administration:FitTrackee administration')}</strong>
</div>
<div className="card-body">
<AdminStats />

View File

@ -2,7 +2,7 @@ import React from 'react'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import { getAppStats } from '../../actions/stats'
import { getAppData } from '../../actions/application'
class AdminStats extends React.Component {
componentDidMount() {
@ -80,7 +80,7 @@ export default withTranslation()(
}),
dispatch => ({
loadAppStats: () => {
dispatch(getAppStats())
dispatch(getAppData('stats/all'))
},
})
)(AdminStats)

View File

@ -0,0 +1,89 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import Message from '../../Common/Message'
import { history } from '../../../index'
import { getFileSize } from '../../../utils'
export default function Config({ appConfig, message, t, updateIsInEdition }) {
return (
<div>
<Helmet>
<title>
FitTrackee - {t('administration:Application configuration')}
</title>
</Helmet>
<Message message={message} t={t} />
<div className="container">
<div className="row">
<div className="col-md-12">
<div className="card">
<div className="card-header">
{t('administration:Application configuration')}
</div>
<div className="card-body">
<div className="row">
<div className="col">
<table className="table">
<tbody>
<tr>
<th scope="row">
{t('administration:Enable registration')}:
</th>
<td>
{appConfig.registration
? t('common:yes')
: t('common:no')}
</td>
</tr>
<tr>
<th scope="row">
{t('administration:Max. number of active users')}:
</th>
<td>{appConfig.max_users}</td>
</tr>
<tr>
<th scope="row">
{t(
'administration:Max. size of ' + 'uploaded files'
)}
:
</th>
<td>{getFileSize(appConfig.max_single_file_size)}</td>
</tr>
<tr>
<th scope="row">
{t('administration:Max. size of zip archive')}:
</th>
<td>{getFileSize(appConfig.max_zip_file_size)}</td>
</tr>
<tr>
<th scope="row">
{t('administration:Max. files of zip archive')}:
</th>
<td>{appConfig.gpx_limit_import}</td>
</tr>
</tbody>
</table>
<input
type="submit"
className="btn btn-primary btn-lg btn-block"
onClick={() => updateIsInEdition()}
value={t('common:Edit')}
/>
<input
type="submit"
className="btn btn-secondary btn-lg btn-block"
onClick={() => history.push('/admin')}
value={t('common:Back')}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,198 @@
import React from 'react'
import { connect } from 'react-redux'
import { Helmet } from 'react-helmet'
import Message from '../../Common/Message'
import { updateAppConfig } from '../../../actions/application'
import { getFileSizeInMB } from '../../../utils'
class AdminApplication extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
formData: {},
}
}
componentDidMount() {
this.initForm()
}
initForm() {
const { appConfig } = this.props
const formData = {}
Object.keys(appConfig).map(k =>
appConfig[k] === null
? (formData[k] = '')
: ['max_single_file_size', 'max_zip_file_size'].includes(k)
? (formData[k] = getFileSizeInMB(appConfig[k]))
: (formData[k] = appConfig[k])
)
this.setState({ formData })
}
handleFormChange(e) {
const { formData } = this.state
if (e.target.name === 'registration') {
formData[e.target.name] = e.target.checked
} else {
formData[e.target.name] = +e.target.value
}
this.setState(formData)
}
render() {
const {
message,
onHandleConfigFormSubmit,
t,
updateIsInEdition,
} = this.props
const { formData } = this.state
return (
<div>
<Helmet>
<title>
FitTrackee - {t('administration:Application configuration')}
</title>
</Helmet>
{message && <Message message={message} t={t} />}
{Object.keys(formData).length > 0 && (
<div className="container">
<div className="row">
<div className="col-md-12">
<div className="card">
<div className="card-header">
{t('administration:Application configuration')}
</div>
<div className="card-body">
<form
className="app-config-form"
onSubmit={event => {
event.preventDefault()
onHandleConfigFormSubmit(formData)
updateIsInEdition()
}}
>
<div className="form-group row">
<label
className="col-sm-6 col-form-label"
htmlFor="registration"
>
{t('administration:Enable registration')}:
</label>
<input
className="col-sm-5"
id="registration"
name="registration"
type="checkbox"
checked={formData.registration}
onChange={e => this.handleFormChange(e)}
/>
</div>
<div className="form-group row">
<label
className="col-sm-6 col-form-label"
htmlFor="max_users"
>
{t('administration:Max. number of active users')}:
</label>
<input
className="col-sm-5"
id="max_users"
name="max_users"
type="number"
min="0"
value={formData.max_users}
onChange={e => this.handleFormChange(e)}
/>
</div>
<div className="form-group row">
<label
className="col-sm-6 col-form-label"
htmlFor="max_single_file_size"
>
{t(
'administration:Max. size of uploaded files (in Mb)'
)}
:
</label>
<input
className="col-sm-5"
id="max_single_file_size"
name="max_single_file_size"
type="number"
step="0.1"
min="0"
value={formData.max_single_file_size}
onChange={e => this.handleFormChange(e)}
/>
</div>
<div className="form-group row">
<label
className="col-sm-6 col-form-label"
htmlFor="max_zip_file_size"
>
{t('administration:Max. size of zip archive (in Mb)')}
:
</label>
<input
className="col-sm-5"
id="max_zip_file_size"
name="max_zip_file_size"
type="number"
step="0.1"
min="0"
value={formData.max_zip_file_size}
onChange={e => this.handleFormChange(e)}
/>
</div>
<div className="form-group row">
<label
className="col-sm-6 col-form-label"
htmlFor="gpx_limit_import"
>
{t('administration:Max. files of zip archive')}
</label>
<input
className="col-sm-5"
id="gpx_limit_import"
name="gpx_limit_import"
type="number"
min="0"
value={formData.gpx_limit_import}
onChange={e => this.handleFormChange(e)}
/>
</div>
<input
type="submit"
className="btn btn-primary btn-lg btn-block"
value={t('common:Submit')}
/>
<input
type="submit"
className="btn btn-secondary btn-lg btn-block"
onClick={() => updateIsInEdition()}
value={t('common:Cancel')}
/>
</form>
</div>
</div>
</div>
</div>
</div>
)}
</div>
)
}
}
export default connect(
() => ({}),
dispatch => ({
onHandleConfigFormSubmit: formData => {
formData.max_single_file_size *= 1048576
formData.max_zip_file_size *= 1048576
dispatch(updateAppConfig(formData))
},
})
)(AdminApplication)

View File

@ -0,0 +1,54 @@
import React from 'react'
import { connect } from 'react-redux'
import { Helmet } from 'react-helmet'
import Config from './Config'
import ConfigForm from './ConfigForm'
import Message from '../../Common/Message'
class AdminApplication extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
isInEdition: false,
}
}
render() {
const { appConfig, message, t } = this.props
const { isInEdition } = this.state
return (
<div>
<Helmet>
<title>FitTrackee - {t('administration:Administration')}</title>
</Helmet>
{message && <Message message={message} t={t} />}
{isInEdition ? (
<ConfigForm
appConfig={appConfig}
message={message}
updateIsInEdition={() => {
this.setState({ isInEdition: false })
}}
t={t}
/>
) : (
<Config
appConfig={appConfig}
message={message}
t={t}
updateIsInEdition={() => {
this.setState({ isInEdition: true })
}}
/>
)}
</div>
)
}
}
export default connect(state => ({
appConfig: state.application.config,
message: state.message,
user: state.user,
}))(AdminApplication)

View File

@ -2,14 +2,13 @@ import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import { Link, Redirect, Route, Switch } from 'react-router-dom'
import { Link, Route, Switch } from 'react-router-dom'
import AdminApplication from './Application'
import AdminDashboard from './AdminDashboard'
import AdminMenu from './AdminMenu'
import AdminSports from './Sports'
import AccessDenied from './../Others/AccessDenied'
import NotFound from './../Others/NotFound'
import { isLoggedIn } from '../../utils'
function Admin(props) {
const { t, user } = props
@ -19,47 +18,48 @@ function Admin(props) {
<title>FitTrackee - {t('administration:Administration')}</title>
</Helmet>
<div className="container dashboard">
<div className="row">
<div className="col-md-3">
<div className="card activity-card">
<div className="card-header">
<Link
to={{
pathname: '/admin/',
}}
>
{t('administration:Administration')}
</Link>
</div>
<div className="card-body">
<AdminMenu t={t} />
{user.admin ? (
<div className="row">
<div className="col-md-3">
<div className="card activity-card">
<div className="card-header">
<Link
to={{
pathname: '/admin/',
}}
>
{t('administration:Administration')}
</Link>
</div>
<div className="card-body">
<AdminMenu t={t} />
</div>
</div>
</div>
<div className="col-md-9">
<Switch>
<Route
exact
path="/admin"
render={() => <AdminDashboard t={t} />}
/>
<Route
exact
path="/admin/application"
render={() => <AdminApplication t={t} />}
/>
<Route
exact
path="/admin/sports"
render={() => <AdminSports t={t} />}
/>
<Route component={NotFound} />
</Switch>
</div>
</div>
<div className="col-md-9">
{isLoggedIn() ? (
user.admin ? (
<Switch>
<Route
exact
path="/admin"
render={() => <AdminDashboard t={t} />}
/>
<Route
exact
path="/admin/sports"
render={() => <AdminSports t={t} />}
/>
<Route component={NotFound} />
</Switch>
) : (
<AccessDenied />
)
) : (
<Redirect to="/login" />
)}
</div>
</div>
) : (
<NotFound />
)}
</div>
</div>
)

View File

@ -177,6 +177,10 @@ label {
margin-left: 10px;
}
.app-config-form label {
font-weight: bold;
}
.card {
text-align: left;
}

View File

@ -1,4 +1,5 @@
import React from 'react'
import { connect } from 'react-redux'
import { Redirect, Route, Switch } from 'react-router-dom'
import './App.css'
@ -14,13 +15,17 @@ import Profile from './User/Profile'
import ProfileEdit from './User/ProfileEdit'
import Statistics from './Statistics'
import UserForm from './User/UserForm'
import { getAppData } from '../actions/application'
import { isLoggedIn } from '../utils'
export default class App extends React.Component {
class App extends React.Component {
constructor(props) {
super(props)
this.props = props
}
componentDidMount() {
this.props.loadAppConfig()
}
render() {
return (
@ -74,7 +79,12 @@ export default class App extends React.Component {
<Route exact path="/activities/history" component={Activities} />
<Route exact path="/activities/statistics" component={Statistics} />
<Route path="/activities" component={Activity} />
<Route path="/admin" component={Admin} />
<Route
path="/admin"
render={() =>
isLoggedIn() ? <Admin /> : <UserForm formType={'login'} />
}
/>
<Route component={NotFound} />
</Switch>
<Footer />
@ -82,3 +92,11 @@ export default class App extends React.Component {
)
}
}
export default connect(
() => ({}),
dispatch => ({
loadAppConfig: () => {
dispatch(getAppData('config'))
},
})
)(App)

View File

@ -1,16 +1,24 @@
import React from 'react'
import { Helmet } from 'react-helmet'
export default function AccessDenied() {
export default function AccessDenied(props) {
const { t } = props
return (
<div>
<Helmet>
<title>FitTrackee - Access denied</title>
<title>FitTrackee - {t('Access denied')}</title>
</Helmet>
<h1 className="page-title">Access denied</h1>
<p className="App-center">
{"You don't have permissions to access this page."}
</p>
<div className="row">
<div className="col-2" />
<div className="card col-8">
<div className="card-body">
<div className="text-center">
{t("You don't have permissions to access this page.")}
</div>
</div>
</div>
<div className="col-2" />
</div>
</div>
)
}

View File

@ -1,13 +1,15 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
export default function NotFound() {
const { t } = useTranslation()
return (
<div>
<Helmet>
<title>fittrackee - 404</title>
</Helmet>
<h1 className="page-title">Page not found</h1>
<h1 className="page-title">{t('Page not found')}</h1>
</div>
)
}

View File

@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'
import { Helmet } from 'react-helmet'
import { history } from '../../index'
import { isRegistrationAllowed } from '../../utils'
export default function Form(props) {
const { t } = useTranslation()
@ -22,7 +21,7 @@ export default function Form(props) {
<div className="col-md-6">
<hr />
<br />
{props.formType === 'register' && !isRegistrationAllowed ? (
{props.formType === 'register' && !props.isRegistrationAllowed ? (
<div className="card">
<div className="card-body">Registration is disabled.</div>
<div className="card-body">

View File

@ -7,15 +7,23 @@ import { Link } from 'react-router-dom'
import Message from '../Common/Message'
import { deletePicture, uploadPicture } from '../../actions/user'
import { apiUrl, fileSizeLimit } from '../../utils'
import { apiUrl, getFileSize } from '../../utils'
function Profile({ message, onDeletePicture, onUploadPicture, t, user }) {
function Profile({
appConfig,
message,
onDeletePicture,
onUploadPicture,
t,
user,
}) {
const createdAt = user.created_at
? format(new Date(user.created_at), 'dd/MM/yyyy HH:mm')
: ''
const birthDate = user.birth_date
? format(new Date(user.birth_date), 'dd/MM/yyyy')
: ''
const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
return (
<div>
<Helmet>
@ -118,6 +126,7 @@ function Profile({ message, onDeletePicture, onUploadPicture, t, user }) {
export default withTranslation()(
connect(
state => ({
appConfig: state.application.config,
message: state.message,
user: state.user,
}),

View File

@ -42,6 +42,7 @@ class UserForm extends React.Component {
render() {
const {
formType,
isRegistrationAllowed,
message,
messages,
onHandleUserFormSubmit,
@ -56,6 +57,7 @@ class UserForm extends React.Component {
<div>
<Message message={message} messages={messages} t={t} />
<Form
isRegistrationAllowed={isRegistrationAllowed}
formType={formType}
userForm={formData}
onHandleFormChange={event => this.onHandleFormChange(event)}
@ -73,6 +75,7 @@ class UserForm extends React.Component {
export default withTranslation()(
connect(
state => ({
isRegistrationAllowed: state.application.config.is_registration_enabled,
location: state.router.location,
message: state.message,
messages: state.messages,