General settings (#28)

adding ui for general settings | adding 'working hours' as new feature
This commit is contained in:
Christian Kellner
2021-05-30 09:37:45 +02:00
committed by GitHub
parent 2899dfc20d
commit 97858b7539
18 changed files with 691 additions and 268 deletions

View File

@@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
import InsufficientPermission from './components/permission/InsufficientPermission';
import PermissionAwareRoute from './components/permission/PermissionAwareRoute';
import GeneralSettings from './views/generalSettings/GeneralSettings';
import ToastsContainer from './components/toasts/ToastContainer';
import JobMutation from './views/jobs/mutation/JobMutation';
import UserMutator from './views/user/mutation/UserMutator';
@@ -78,6 +79,12 @@ export default function FredyApp() {
currentUser={currentUser}
/>
<PermissionAwareRoute name="Users" path="/users" component={<Users />} currentUser={currentUser} />
<PermissionAwareRoute
name="General Settings"
path="/generalSettings"
component={<GeneralSettings />}
currentUser={currentUser}
/>
<Redirect from="/" to={'/jobs'} />
</Switch>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { Menu } from 'semantic-ui-react';
import { Icon, Menu } from 'semantic-ui-react';
import './Menu.less';
import { useLocation } from 'react-router';
@@ -19,7 +19,7 @@ const TopMenu = function TopMenu({ isAdmin }) {
className={isActiveRoute('jobs') ? 'topMenu__active' : 'topMenu__item'}
onClick={() => history.push('/jobs')}
>
Job Configuration
<Icon name="search" /> Job Configuration
</Menu.Item>
{isAdmin && (
@@ -29,7 +29,18 @@ const TopMenu = function TopMenu({ isAdmin }) {
className={isActiveRoute('users') ? 'topMenu__active' : 'topMenu__item'}
onClick={() => history.push('/users')}
>
User configuration
<Icon name="user" /> User configuration
</Menu.Item>
)}
{isAdmin && (
<Menu.Item
name="general"
active={isActiveRoute('general')}
className={isActiveRoute('general') ? 'topMenu__active' : 'topMenu__item'}
onClick={() => history.push('/generalSettings')}
>
<Icon name="cog" /> General Settings
</Menu.Item>
)}
</Menu>

View File

@@ -0,0 +1,26 @@
import { xhrGet } from '../../xhr';
export const generalSettings = {
state: {
settings: {},
},
reducers: {
//only admins
setGeneralSettings: (state, payload) => {
return {
...state,
settings: payload,
};
},
},
effects: {
async getGeneralSettings() {
try {
const response = await xhrGet('/api/admin/generalSettings');
this.setGeneralSettings(response.json);
} catch (Exception) {
console.error('Error while trying to get resource for api/admin/generalSettings. Error:', Exception);
}
},
},
};

View File

@@ -1,4 +1,5 @@
import { notificationAdapter } from './models/notificationAdapter';
import { generalSettings } from './models/generalSettings';
import createLoadingPlugin from '@rematch/loading';
import { provider } from './models/provider';
import { createLogger } from 'redux-logger';
@@ -17,6 +18,7 @@ const store = init({
name: 'fredy',
models: {
notificationAdapter,
generalSettings,
provider,
jobs,
user,

View File

@@ -0,0 +1,211 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Button, Form, Header, Icon, Message, Popup, Segment } from 'semantic-ui-react';
import ToastContext from '../../components/toasts/ToastContext';
import Headline from '../../components/headline/Headline';
import { xhrPost } from '../../services/xhr';
import './GeneralSettings.less';
const SegmentPart = ({ name, icon, children, helpText }) => (
<React.Fragment>
<Header as="h5" inverted attached="top" sub>
<Icon name={icon} inverted size="mini" />
<Header.Content>{name}</Header.Content>
</Header>
<Popup
content={helpText}
trigger={
<span className="generalSettings__help">
{' '}
<Icon name="help circle" inverted />
What is this?
</span>
}
/>
<Segment inverted attached>
{children}
</Segment>
</React.Fragment>
);
const GeneralSettings = function Users() {
const dispatch = useDispatch();
const [loading, setLoading] = React.useState(true);
const settings = useSelector((state) => state.generalSettings.settings);
const [interval, setInterval] = React.useState('');
const [port, setPort] = React.useState('');
const [scrapingAntApiKey, setScrapingAntApiKey] = React.useState('');
const [workingHourFrom, setWorkingHourFrom] = React.useState(null);
const [workingHourTo, setWorkingHourTo] = React.useState(null);
const ctx = React.useContext(ToastContext);
React.useEffect(async () => {
await dispatch.generalSettings.getGeneralSettings();
setLoading(false);
}, []);
React.useEffect(async () => {
setInterval(settings?.interval);
setPort(settings?.port);
setScrapingAntApiKey(settings?.scrapingAnt?.apiKey);
setWorkingHourFrom(settings?.workingHours?.from);
setWorkingHourTo(settings?.workingHours?.to);
}, [settings]);
const nullOrEmpty = (val) => val == null || val.length === 0;
const throwMessage = (message, type) => {
ctx.showToast({
title: type === 'error' ? 'Error' : 'Success',
message: message,
delay: 5000,
backgroundColor: type === 'error' ? '#db2828' : '#87eb8f',
color: type === 'error' ? '#fff' : '#000',
});
};
const onStore = async () => {
if (nullOrEmpty(interval)) {
throwMessage('Interval may not be empty.', 'error');
return;
}
if (nullOrEmpty(port)) {
throwMessage('Port may not be empty.', 'error');
return;
}
if (
(!nullOrEmpty(workingHourFrom) && nullOrEmpty(workingHourTo)) ||
(nullOrEmpty(workingHourFrom) && !nullOrEmpty(workingHourTo))
) {
throwMessage('Working hours to and from must be set if either to or from has been set before.', 'error');
return;
}
try {
await xhrPost('/api/admin/generalSettings', {
interval,
port,
scrapingAnt: {
apiKey: scrapingAntApiKey,
},
workingHours: {
from: workingHourFrom,
to: workingHourTo,
},
});
} catch (exception) {
console.error(exception);
throwMessage('Error while trying to store settings.', 'error');
return;
}
throwMessage('Settings stored successfully. You MUST restart Fredy.', 'success');
};
return (
<div>
{!loading && (
<React.Fragment>
<Headline text="General Settings" />
<Message info>
<h5>
<Icon name="info circle" />
Info
</h5>
<p>If you change any settings, you must restart Fredy afterwards.</p>
</Message>
<Form>
<SegmentPart
name="Interval"
helpText="Interval in minutes for running queries against the configured services."
icon="refresh"
>
<Form.Input
type="number"
min="0"
max="1440"
placeholder="Interval in minutes"
inverted
size="mini"
width={6}
defaultValue={interval}
onChange={(e) => setInterval(e.target.value)}
/>
</SegmentPart>
<SegmentPart name="Port" helpText="Port on which Fredy is running." icon="connectdevelop">
<Form.Input
type="number"
min="0"
max="99999"
placeholder="Port"
inverted
size="mini"
width={6}
defaultValue={port}
onChange={(e) => setPort(e.target.value)}
/>
</SegmentPart>
<SegmentPart
name="ScrapingAnt Api Key"
helpText="The api key for ScrapingAnt is used to be able to scrape Immoscout."
icon="key"
>
<Form.Input
type="text"
placeholder="ScrapingAnt Api Key"
inverted
size="mini"
width={6}
defaultValue={scrapingAntApiKey}
onChange={(e) => setScrapingAntApiKey(e.target.value)}
/>
</SegmentPart>
<SegmentPart
name="Working hours"
helpText="During this hours, Fredy will search for new apartments. If nothing is configured, Fredy will search around the clock."
icon="calendar outline"
>
<div className="generalSettings__timePickerContainer">
<Form.Input
className="generalSettings__time"
type="time"
placeholder="ScrapingAnt Api Key"
inverted
size="mini"
width={2}
defaultValue={workingHourFrom}
onChange={(e) => setWorkingHourFrom(e.target.value)}
/>
<div className="generalSettings__until">until</div>
<Form.Input
type="time"
placeholder="ScrapingAnt Api Key"
inverted
size="mini"
width={2}
defaultValue={workingHourTo}
onChange={(e) => setWorkingHourTo(e.target.value)}
/>
</div>
</SegmentPart>
<Segment inverted floated="right">
<Button color="teal" onClick={onStore}>
Save
</Button>
</Segment>
</Form>
</React.Fragment>
)}
</div>
);
};
export default GeneralSettings;

View File

@@ -0,0 +1,17 @@
.generalSettings {
&__timePickerContainer {
display: flex;
align-items: baseline;
}
&__until {
margin-left: 1rem;
margin-right: 1rem;
}
&__help{
font-size: 11px;
margin-left: 1rem;
}
}