Modernizing ui (#73)

Modernizing ui
This commit is contained in:
Christian Kellner
2023-03-20 08:52:13 +01:00
committed by GitHub
parent 2c5eceb0c1
commit d7c9c4bf76
45 changed files with 1233 additions and 1275 deletions

View File

@@ -1,13 +1,12 @@
import React from 'react';
import ToastContext from '../../components/toasts/ToastContext';
import JobTable from '../../components/table/JobTable';
import { useSelector, useDispatch } from 'react-redux';
import { xhrDelete, xhrPut } from '../../services/xhr';
import { Button, Icon } from 'semantic-ui-react';
import { useHistory } from 'react-router-dom';
import ProcessingTimes from './ProcessingTimes';
import { Button, Toast } from '@douyinfe/semi-ui';
import { IconPlusCircle } from '@douyinfe/semi-icons';
import './Jobs.less';
export default function Jobs() {
@@ -15,49 +14,24 @@ export default function Jobs() {
const processingTimes = useSelector((state) => state.jobs.processingTimes);
const history = useHistory();
const dispatch = useDispatch();
const ctx = React.useContext(ToastContext);
const onJobRemoval = async (jobId) => {
try {
await xhrDelete('/api/jobs', { jobId });
ctx.showToast({
title: 'Success',
message: 'Job successfully remove',
delay: 5000,
backgroundColor: '#87eb8f',
color: '#000',
});
Toast.success('Job successfully remove');
await dispatch.jobs.getJobs();
} catch (error) {
ctx.showToast({
title: 'Error',
message: error,
delay: 35000,
backgroundColor: '#db2828',
color: '#fff',
});
Toast.error(error);
}
};
const onJobStatusChanged = async (jobId, status) => {
try {
await xhrPut(`/api/jobs/${jobId}/status`, { status });
ctx.showToast({
title: 'Success',
message: 'Job status successfully changed',
delay: 5000,
backgroundColor: '#87eb8f',
color: '#000',
});
Toast.success('Job status successfully changed');
await dispatch.jobs.getJobs();
} catch (error) {
ctx.showToast({
title: 'Error',
message: error,
delay: 35000,
backgroundColor: '#db2828',
color: '#fff',
});
Toast.error(error);
}
};
@@ -65,8 +39,12 @@ export default function Jobs() {
<div>
<div>
{processingTimes != null && <ProcessingTimes processingTimes={processingTimes} />}
<Button primary className="jobs__newButton" onClick={() => history.push('/jobs/new')}>
<Icon name="plus" />
<Button
type="primary"
icon={<IconPlusCircle />}
className="jobs__newButton"
onClick={() => history.push('/jobs/new')}
>
New Job
</Button>
</div>

View File

@@ -1,51 +1,67 @@
import React from 'react';
import { format } from '../../services/time/timeService';
import { Header, Label, Message, Segment } from 'semantic-ui-react';
import { Card, Descriptions, Divider } from '@douyinfe/semi-ui';
import { IconBolt } from '@douyinfe/semi-icons';
export default function ProcessingTimes({ processingTimes }) {
const { Meta } = Card;
return (
<React.Fragment>
<div>
<Label as="span" color="black">
Processing Interval:
<Label.Detail>{processingTimes.interval} min</Label.Detail>
</Label>
<>
<Descriptions
row
size="small"
style={{
backgroundColor: '#35363c',
borderRadius: '4px',
padding: '10px',
}}
>
<Descriptions.Item itemKey="Processing Interval">{processingTimes.interval} min</Descriptions.Item>
{processingTimes.lastRun && (
<React.Fragment>
<Label as="span" color="black">
Last run:
<Label.Detail>{format(processingTimes.lastRun)}</Label.Detail>
</Label>
<Label as="span" color="black">
Next run:
<Label.Detail>{format(processingTimes.lastRun + processingTimes.interval * 60000)}</Label.Detail>
</Label>
</React.Fragment>
<>
<Descriptions.Item itemKey="Last run">{format(processingTimes.lastRun)}</Descriptions.Item>
<Descriptions.Item itemKey="Next run">
{format(processingTimes.lastRun + processingTimes.interval * 60000)}
</Descriptions.Item>
</>
)}
</div>
</Descriptions>
{processingTimes.scrapingAntData != null && (
<Segment inverted>
<Header as="h5">Remaining ScrapingAnt calls</Header>
<Message.List>
<Message.Item>Plan: {processingTimes.scrapingAntData.plan_name}</Message.Item>
<Message.Item>
<>
<Divider margin="1rem" />
<Card
style={{ backgroundColor: '#35363c' }}
title={
<Meta
title="Remaining ScrapingAnt calls"
description="Information about your Scraping Ant Plan"
avatar={<IconBolt />}
/>
}
>
<p>Plan: {processingTimes.scrapingAntData.plan_name}</p>
<p>
Duration: {format(new Date(processingTimes.scrapingAntData.start_date))} -{' '}
{format(new Date(processingTimes.scrapingAntData.end_date))}
</Message.Item>
<Message.Item>
<br />
Credits: {processingTimes.scrapingAntData.remained_credits}/
{processingTimes.scrapingAntData.plan_total_credits} (250 credits per call)
</Message.Item>
</Message.List>
If you want to scrape Immoscout more often, you have to purchase a premium account of{' '}
<a href="https://scrapingant.com/" target="_blank" rel="noreferrer">
{' '}
ScrapingAnt
</a>
. You can use the code <b>FREDY10</b> to get 10% off. (No affiliation, we are <b>not</b> getting paid to
recommend ScrapingAnt.)
</Segment>
</p>
If you want to scrape Immoscout more often, you have to purchase a premium account of{' '}
<a href="https://scrapingant.com/" target="_blank" rel="noreferrer">
ScrapingAnt
</a>
. You can use the code <b>FREDY10</b> to get 10% off. (No affiliation, we are <b>not</b> getting paid to
recommend ScrapingAnt.)
</Card>
</>
)}
</React.Fragment>
</>
);
}
/*
*/

View File

@@ -2,19 +2,17 @@ import React, { Fragment, useState } from 'react';
import NotificationAdapterMutator from './components/notificationAdapter/NotificationAdapterMutator';
import NotificationAdapterTable from '../../../components/table/NotificationAdapterTable';
import { Icon, Form, Button, Label } from 'semantic-ui-react';
import ProviderTable from '../../../components/table/ProviderTable';
import ProviderMutator from './components/provider/ProviderMutator';
import ToastContext from '../../../components/toasts/ToastContext';
import Headline from '../../../components/headline/Headline';
import { useDispatch, useSelector } from 'react-redux';
import { xhrPost } from '../../../services/xhr';
import { useHistory } from 'react-router-dom';
import { useParams } from 'react-router';
import { Divider, Input, Switch, Button, TagInput, Toast } from '@douyinfe/semi-ui';
import './JobMutation.less';
import Switch from 'react-switch';
import { SegmentPart } from '../../../components/segment/SegmentPart';
import { IconPlusCircle } from '@douyinfe/semi-icons';
export default function JobMutator() {
const jobs = useSelector((state) => state.jobs.jobs);
@@ -38,7 +36,6 @@ export default function JobMutator() {
const [enabled, setEnabled] = useState(defaultEnabled);
const history = useHistory();
const dispatch = useDispatch();
const ctx = React.useContext(ToastContext);
const isSavingEnabled = () => {
return notificationAdapterData.length > 0 && providerData.length > 0 && name != null && name.length > 0;
@@ -55,24 +52,11 @@ export default function JobMutator() {
jobId: jobToBeEdit?.id || null,
});
await dispatch.jobs.getJobs();
ctx.showToast({
title: 'Success',
message: 'Job successfully saved...',
delay: 5000,
backgroundColor: '#87eb8f',
color: '#000',
});
Toast.success('Job successfully saved...');
history.push('/jobs');
} catch (Exception) {
console.error(Exception.json.message);
ctx.showToast({
title: 'Error',
message: Exception.json != null ? Exception.json.message : Exception,
delay: 8000,
backgroundColor: '#db2828',
color: '#fff',
});
Toast.error(Exception.json != null ? Exception.json.message : Exception);
}
};
@@ -107,20 +91,19 @@ export default function JobMutator() {
)}
<Headline text={jobToBeEdit ? 'Edit a Job' : 'Create a new Job'} />
<Form>
<form>
<SegmentPart name="Name">
<Form.Input
<Input
autofocus
type="text"
maxLength={40}
placeholder="Name"
autoFocus
inverted
width={6}
defaultValue={name}
onChange={(e) => setName(e.target.value)}
value={name}
onChange={(value) => setName(value)}
/>
</SegmentPart>
<Divider margin="1rem" />
<SegmentPart
name="Provider"
icon="briefcase"
@@ -130,10 +113,14 @@ export default function JobMutator() {
'to search for new listings.'
}
>
<Form.Button primary className="jobMutation__newButton" onClick={() => setProviderCreationVisibility(true)}>
<Icon name="plus" />
<Button
type="primary"
icon={<IconPlusCircle />}
className="jobMutation__newButton"
onClick={() => setProviderCreationVisibility(true)}
>
Add new Provider
</Form.Button>
</Button>
<ProviderTable
providerData={providerData}
@@ -142,20 +129,20 @@ export default function JobMutator() {
}}
/>
</SegmentPart>
<Divider margin="1rem" />
<SegmentPart
icon="bell"
name="Notification Adapter"
helpText="Fredy supports multiple ways to notify you about new findings. These are called notification adapter. You can chose between email, Telegram etc."
>
<Form.Button
primary
<Button
type="primary"
className="jobMutation__newButton"
icon={<IconPlusCircle />}
onClick={() => setNotificationCreationVisibility(true)}
>
<Icon name="plus" />
Add new Notification Adapter
</Form.Button>
</Button>
<NotificationAdapterTable
notificationAdapter={notificationAdapterData}
@@ -169,40 +156,19 @@ export default function JobMutator() {
}}
/>
</SegmentPart>
<Divider margin="1rem" />
<SegmentPart
icon="bell"
name="Blacklist"
helpText="If a listing contains one of these words, it will be filtered out. Words must be comma separated. To remove a word from the black list, just click the red label(s)."
helpText="If a listing contains one of these words, it will be filtered out. Type in a word, then hit enter."
>
<Form.Input
type="text"
maxLength={40}
placeholder="Comma separated list of blacklisted words"
autoFocus
inverted
width={6}
onChange={(e) => {
if (e.target.value.indexOf(',') !== -1) {
setBlacklist([...blacklist, e.target.value.replace(',', '')]);
e.target.value = '';
}
}}
<TagInput
value={blacklist || []}
placeholder="Add a word for filtering..."
onChange={(v) => setBlacklist([...v])}
/>
{blacklist.map((blacklistWord) => (
<Label
as="a"
key={`blacklist_${blacklistWord}`}
onClick={(e, obj) => {
setBlacklist(blacklist.filter((word) => word !== obj.content));
}}
content={blacklistWord}
icon="thumbs down"
color="red"
/>
))}
</SegmentPart>
<Divider margin="1rem" />
<SegmentPart
icon="play circle outline"
name="Job activation"
@@ -210,14 +176,14 @@ export default function JobMutator() {
>
<Switch className="jobMutation__spaceTop" onChange={(checked) => setEnabled(checked)} checked={enabled} />
</SegmentPart>
<Button color="red" onClick={() => history.push('/jobs')}>
<Divider margin="1rem" />
<Button type="danger" style={{ marginRight: '1rem' }} onClick={() => history.push('/jobs')}>
Cancel
</Button>
<Button color="green" disabled={!isSavingEnabled()} onClick={mutateJob}>
<Button type="primary" icon={<IconPlusCircle />} disabled={!isSavingEnabled()} onClick={mutateJob}>
Save
</Button>
</Form>
</form>
</Fragment>
);
}

View File

@@ -1,5 +1,10 @@
.jobMutation {
&__newButton{
float: right;
margin-bottom: 1rem;
}
}
.semi-select-option-list-wrapper {
width: 25rem;
}

View File

@@ -1,11 +1,10 @@
import React, { useState } from 'react';
import { transform } from '../../../../../services/transformer/notificationAdapterTransformer';
import { Modal, Form, Button, Dropdown, Input, Message } from 'semantic-ui-react';
import { xhrPost } from '../../../../../services/xhr';
import Help from './NotificationHelpDisplay';
import { useSelector } from 'react-redux';
import Switch from 'react-switch';
import { Banner, Button, Form, Modal, Select, Switch } from '@douyinfe/semi-ui';
import './NotificationAdapterMutator.less';
@@ -138,8 +137,7 @@ export default function NotificationAdapterMutator({
const uiElement = selectedAdapter.fields[key];
return (
<Form.Field key={uiElement.description}>
<label>{uiElement.label}:</label>
<Form key={key}>
{uiElement.type === 'boolean' ? (
<Switch
checked={uiElement.value || false}
@@ -148,106 +146,108 @@ export default function NotificationAdapterMutator({
}}
/>
) : (
<Input
<Form.Input
style={{ width: '100%' }}
field={uiElement.label}
type={uiElement.type}
value={uiElement.value || ''}
placeholder={uiElement.label}
onChange={(e) => {
setValue(selectedAdapter, uiElement, key, e.target.value);
label={uiElement.label}
onChange={(value) => {
setValue(selectedAdapter, uiElement, key, value);
}}
/>
)}
</Form.Field>
</Form>
);
});
};
return (
<Modal
onClose={() => onVisibilityChanged(false)}
onOpen={() => onVisibilityChanged(true)}
open={visible}
title="Adding a new Notification Adapter"
visible={visible}
style={{ width: '95%' }}
footer={
<div>
<Button type="secondary" disabled={selectedAdapter == null} style={{ float: 'left' }} onClick={() => onTry()}>
Try
</Button>
<Button type="danger" onClick={() => onSubmit(true)}>
Save
</Button>
<Button type="primary" onClick={() => onSubmit(false)}>
Cancel
</Button>
</div>
}
>
<Modal.Header>Adding a new Notification Adapter</Modal.Header>
<Modal.Content image>
<Modal.Description>
{validationMessage != null && (
<Message negative>
<Message.Header>Houston we have a problem...</Message.Header>
<p dangerouslySetInnerHTML={{ __html: validationMessage }} />
</Message>
)}
{successMessage != null && (
<Message positive>
<Message.Header>Yay!</Message.Header>
<p dangerouslySetInnerHTML={{ __html: successMessage }} />
</Message>
)}
<p>
When Fredy found new listings, we like to report them to you. To do so, notification adapter can be
configured. <br />
There are multiple ways how Fredy can send new listings to you. Chose your weapon...
</p>
<Dropdown
placeholder="Select a notification adapter"
className="providerMutator__fields"
selection
value={selectedAdapter == null ? '' : selectedAdapter.id}
options={adapter
.map((a) => {
return {
key: a.id,
value: a.id,
text: a.name,
};
})
//filter out those, that have already been selected
.filter((option) =>
editNotificationAdapter != null
? true
: selected.find((selectedOption) => selectedOption.id === option.key) == null
)
.sort(sortAdapter)}
onChange={(e, { value }) => {
setSuccessMessage(null);
setValidationMessage(null);
const selectedAdapter = adapter.find((a) => a.id === value);
setSelectedAdapter(Object.assign({}, selectedAdapter));
}}
/>
<br />
<br />
{selectedAdapter != null && (
<Form>
<i>{selectedAdapter.description}</i>
<br />
{selectedAdapter.readme != null && (
<React.Fragment>
<Help readme={selectedAdapter.readme} />
</React.Fragment>
)}
<br />
{getFieldsFor(selectedAdapter)}
</Form>
)}
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button
content="Try Notification Adapter"
labelPosition="left"
floated="left"
icon="hand spock"
onClick={() => onTry()}
color="teal"
{validationMessage != null && (
<Banner
fullMode={false}
type="danger"
closeIcon={null}
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Error</div>}
style={{ marginBottom: '1rem' }}
description={<p dangerouslySetInnerHTML={{ __html: validationMessage }} />}
/>
<Button color="black" onClick={() => onSubmit(false)}>
Cancel
</Button>
<Button content="Save" labelPosition="right" icon="checkmark" onClick={() => onSubmit(true)} positive />
</Modal.Actions>
)}
{successMessage != null && (
<Banner
fullMode={false}
type="success"
closeIcon={null}
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Yay!</div>}
style={{ marginBottom: '1rem' }}
description={<p dangerouslySetInnerHTML={{ __html: successMessage }} />}
/>
)}
<p>
When Fredy found new listings, we like to report them to you. To do so, notification adapter can be configured.{' '}
<br />
There are multiple ways how Fredy can send new listings to you. Chose your weapon...
</p>
<Select
filter
placeholder="Select a notification adapter"
className="providerMutator__fields"
value={selectedAdapter == null ? '' : selectedAdapter.id}
optionList={adapter
.map((a) => {
return {
otherKey: a.id,
value: a.id,
label: a.name,
};
})
//filter out those, that have already been selected
.filter((option) =>
editNotificationAdapter != null
? true
: selected.find((selectedOption) => selectedOption.id === option.key) == null
)
.sort(sortAdapter)}
onChange={(value) => {
setSuccessMessage(null);
setValidationMessage(null);
const selectedAdapter = adapter.find((a) => a.id === value);
setSelectedAdapter(Object.assign({}, selectedAdapter));
}}
/>
<br />
<br />
{selectedAdapter != null && (
<>
<i>{selectedAdapter.description}</i>
<br />
<br />
{selectedAdapter.readme != null && <Help readme={selectedAdapter.readme} />}
<br />
{getFieldsFor(selectedAdapter)}
</>
)}
</Modal>
);
}

View File

@@ -1,19 +1,14 @@
import React from 'react';
import { Accordion, Icon } from 'semantic-ui-react';
import { Banner } from '@douyinfe/semi-ui';
export default function Help({ readme }) {
const [active, setActive] = React.useState(false);
return (
<Accordion>
<Accordion.Title active={active} index={0} onClick={() => setActive(!active)}>
<React.Fragment>
<Icon name="dropdown" /> <span className="providerMutator__helpLink"> More information</span>
</React.Fragment>
</Accordion.Title>
<Accordion.Content active={active} className="providerMutator__helpBox">
<p dangerouslySetInnerHTML={{ __html: readme }} />
</Accordion.Content>
</Accordion>
<Banner
fullMode={false}
type="info"
closeIcon={null}
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Information</div>}
description={<p dangerouslySetInnerHTML={{ __html: readme }} />}
/>
);
}

View File

@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import { Banner, Modal, Select, Input } from '@douyinfe/semi-ui';
import { transform } from '../../../../../services/transformer/providerTransformer';
import { Modal, Icon, Button, Dropdown, Input, Message } from 'semantic-ui-react';
import { useSelector } from 'react-redux';
import { IconLikeHeart } from '@douyinfe/semi-icons';
import './ProviderMutator.less';
const sortProvider = (a, b) => {
@@ -61,79 +61,90 @@ export default function ProviderMutator({ onVisibilityChanged, visible = false,
};
return (
<Modal onClose={() => onVisibilityChanged(false)} onOpen={() => onVisibilityChanged(true)} open={visible}>
<Modal.Header>Adding a new Provider</Modal.Header>
<Modal.Content image>
<Modal.Description>
{validationMessage != null && (
<Message negative>
<Message.Header>Houston we have a problem...</Message.Header>
<p>{validationMessage}</p>
</Message>
)}
<Modal
title="Adding a new Provider"
visible={visible}
onOk={() => onSubmit(true)}
onCancel={() => onSubmit(false)}
style={{ width: '50rem' }}
okText="Save"
>
{validationMessage != null && (
<Banner
fullMode={false}
type="danger"
closeIcon={null}
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Error</div>}
style={{ marginBottom: '1rem' }}
description={validationMessage}
/>
)}
<p>
Provider are the <Icon name="heart" color="red" /> of Fredy. We're supporting multiple Provider such as
Immowelt, Kalaydo etc. Select a provider from the list below.
<br />
Fredy will then open the provider's url in a new tab.
</p>
<p>
You will need to configure your search parameter like you would do when you do a regular search on the
provider's website.
<br />
When the search results are shown on the website, copy the url and paste it into the textfield below.
<br />
<span style={{ color: '#ff0000' }}>
<p>
Provider are the <IconLikeHeart style={{ color: '#ff0000' }} /> of Fredy. We're supporting multiple Provider
such as Immowelt, Kalaydo etc. Select a provider from the list below.
<br />
Fredy will then open the provider's url in a new tab.
</p>
<p>
You will need to configure your search parameter like you would do when you do a regular search on the
provider's website.
<br />
When the search results are shown on the website, copy the url and paste it into the textfield below.
</p>
<Banner
fullMode={false}
type="warning"
closeIcon={null}
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>ScrapingAnt</div>}
style={{ marginBottom: '1rem' }}
description={
<div>
<p>
If you chose Immoscout as a provider, make sure to also add the scrapingAnt apiKey to the config.json.
(See readme)
</span>
<br />
<span style={{ color: '#ff0000' }}>
</p>
<p>
Do not forget to sort the results by date before copying the url to Fredy, so that Fredy always captures
the latest search results.
</span>
</p>
<Dropdown
placeholder="Select a provider"
className="providerMutator__fields"
selection
value={selectedProvider == null ? '' : selectedProvider.id}
options={provider
.map((pro) => {
return {
key: pro.id,
value: pro.id,
text: pro.name,
};
})
.sort(sortProvider)}
onChange={(e, { value }) => {
const selectedProvider = provider.find((pro) => pro.id === value);
setSelectedProvider(selectedProvider);
</p>
</div>
}
/>
window.open(selectedProvider.baseUrl);
}}
/>
<br />
<br />
<Input
type="text"
placeholder="Provider Url"
width={10}
className="providerMutator__fields"
onBlur={(e) => {
setProviderUrl(e.target.value);
}}
/>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button color="black" onClick={() => onSubmit(false)}>
Cancel
</Button>
<Button content="Save" labelPosition="right" icon="checkmark" onClick={() => onSubmit(true)} positive />
</Modal.Actions>
<Select
filter
placeholder="Select a provider"
className="providerMutator__fields"
optionList={provider
.map((pro) => {
return {
otherKey: pro.id,
value: pro.id,
label: pro.name,
};
})
.sort(sortProvider)}
style={{ width: 180 }}
value={selectedProvider == null ? '' : selectedProvider.id}
onChange={(value) => {
const selectedProvider = provider.find((pro) => pro.id === value);
setSelectedProvider(selectedProvider);
window.open(selectedProvider.baseUrl);
}}
/>
<br />
<br />
<Input
type="text"
placeholder="Provider Url"
width={10}
className="providerMutator__fields"
onBlur={(e) => {
setProviderUrl(e.target.value);
}}
/>
</Modal>
);
}