mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
chore: run formatter (#145)
This commit is contained in:
64
index.js
64
index.js
@@ -1,14 +1,14 @@
|
||||
import fs from 'fs';
|
||||
import {config} from './lib/utils.js';
|
||||
import { config } from './lib/utils.js';
|
||||
import * as similarityCache from './lib/services/similarity-check/similarityCache.js';
|
||||
import { setLastJobExecution } from './lib/services/storage/listingsStorage.js';
|
||||
import * as jobStorage from './lib/services/storage/jobStorage.js';
|
||||
import FredyRuntime from './lib/FredyRuntime.js';
|
||||
import { duringWorkingHoursOrNotSet } from './lib/utils.js';
|
||||
import './lib/api/api.js';
|
||||
import {track} from './lib/services/tracking/Tracker.js';
|
||||
import {handleDemoUser} from './lib/services/storage/userStorage.js';
|
||||
import {cleanupDemoAtMidnight} from './lib/services/demoCleanup.js';
|
||||
import { track } from './lib/services/tracking/Tracker.js';
|
||||
import { handleDemoUser } from './lib/services/storage/userStorage.js';
|
||||
import { cleanupDemoAtMidnight } from './lib/services/demoCleanup.js';
|
||||
//if db folder does not exist, ensure to create it before loading anything else
|
||||
if (!fs.existsSync('./db')) {
|
||||
fs.mkdirSync('./db');
|
||||
@@ -19,13 +19,13 @@ const provider = fs.readdirSync(path).filter((file) => file.endsWith('.js'));
|
||||
const INTERVAL = config.interval * 60 * 1000;
|
||||
/* eslint-disable no-console */
|
||||
console.log(`Started Fredy successfully. Ui can be accessed via http://localhost:${config.port}`);
|
||||
if(config.demoMode){
|
||||
console.info('Running in demo mode');
|
||||
cleanupDemoAtMidnight();
|
||||
if (config.demoMode) {
|
||||
console.info('Running in demo mode');
|
||||
cleanupDemoAtMidnight();
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
const fetchedProvider = await Promise.all(
|
||||
provider.filter((provider) => provider.endsWith('.js')).map(async (pro) => import(`${path}/${pro}`))
|
||||
provider.filter((provider) => provider.endsWith('.js')).map(async (pro) => import(`${path}/${pro}`)),
|
||||
);
|
||||
|
||||
handleDemoUser();
|
||||
@@ -33,30 +33,30 @@ handleDemoUser();
|
||||
setInterval(
|
||||
(function exec() {
|
||||
const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now());
|
||||
if(!config.demoMode) {
|
||||
if (isDuringWorkingHoursOrNotSet) {
|
||||
track();
|
||||
config.lastRun = Date.now();
|
||||
jobStorage
|
||||
.getJobs()
|
||||
.filter((job) => job.enabled)
|
||||
.forEach((job) => {
|
||||
job.provider
|
||||
.filter((p) => fetchedProvider.find((fp) => fp.metaInformation.id === p.id) != null)
|
||||
.forEach(async (prov) => {
|
||||
const pro = fetchedProvider.find((fp) => fp.metaInformation.id === prov.id);
|
||||
pro.init(prov, job.blacklist);
|
||||
await new FredyRuntime(pro.config, job.notificationAdapter, prov.id, job.id, similarityCache).execute();
|
||||
setLastJobExecution(job.id);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
/* eslint-disable no-console */
|
||||
console.debug('Working hours set. Skipping as outside of working hours.');
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
}
|
||||
if (!config.demoMode) {
|
||||
if (isDuringWorkingHoursOrNotSet) {
|
||||
track();
|
||||
config.lastRun = Date.now();
|
||||
jobStorage
|
||||
.getJobs()
|
||||
.filter((job) => job.enabled)
|
||||
.forEach((job) => {
|
||||
job.provider
|
||||
.filter((p) => fetchedProvider.find((fp) => fp.metaInformation.id === p.id) != null)
|
||||
.forEach(async (prov) => {
|
||||
const pro = fetchedProvider.find((fp) => fp.metaInformation.id === prov.id);
|
||||
pro.init(prov, job.blacklist);
|
||||
await new FredyRuntime(pro.config, job.notificationAdapter, prov.id, job.id, similarityCache).execute();
|
||||
setLastJobExecution(job.id);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
/* eslint-disable no-console */
|
||||
console.debug('Working hours set. Skipping as outside of working hours.');
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
}
|
||||
return exec;
|
||||
})(),
|
||||
INTERVAL
|
||||
INTERVAL,
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import restana from 'restana';
|
||||
import files from 'serve-static';
|
||||
import path from 'path';
|
||||
import { getDirName } from '../utils.js';
|
||||
import {demoRouter} from './routes/demoRouter.js';
|
||||
import { demoRouter } from './routes/demoRouter.js';
|
||||
const service = restana();
|
||||
const staticService = files(path.join(getDirName(), '../ui/public'));
|
||||
const PORT = config.port || 9998;
|
||||
|
||||
@@ -6,7 +6,7 @@ const adapter = await Promise.all(
|
||||
fs
|
||||
.readdirSync('./lib/notification/adapter')
|
||||
.filter((file) => file.endsWith('.js'))
|
||||
.map(async (integPath) => await import(`${path}/${integPath}`))
|
||||
.map(async (integPath) => await import(`${path}/${integPath}`)),
|
||||
);
|
||||
|
||||
if (adapter.length === 0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import utils, {buildHash} from '../utils.js';
|
||||
import utils, { buildHash } from '../utils.js';
|
||||
let appliedBlackList = [];
|
||||
function shortenLink(link) {
|
||||
return link.substring(0, link.indexOf('?'));
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import utils, {buildHash} from '../utils.js';
|
||||
import utils, { buildHash } from '../utils.js';
|
||||
|
||||
let appliedBlackList = [];
|
||||
let appliedBlacklistedDistricts = [];
|
||||
|
||||
function normalize(o) {
|
||||
const size = o.size || '--- m²';
|
||||
const id = buildHash(o.id, o.price);
|
||||
const link = `https://www.kleinanzeigen.de${o.link}`;
|
||||
return Object.assign(o, {id, size, link});
|
||||
const size = o.size || '--- m²';
|
||||
const id = buildHash(o.id, o.price);
|
||||
const link = `https://www.kleinanzeigen.de${o.link}`;
|
||||
return Object.assign(o, { id, size, link });
|
||||
}
|
||||
|
||||
function applyBlacklist(o) {
|
||||
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
||||
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
||||
const isBlacklistedDistrict =
|
||||
appliedBlacklistedDistricts.length === 0 ? false : utils.isOneOf(o.description, appliedBlacklistedDistricts);
|
||||
return o.title != null && !isBlacklistedDistrict && titleNotBlacklisted && descNotBlacklisted;
|
||||
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
||||
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
||||
const isBlacklistedDistrict =
|
||||
appliedBlacklistedDistricts.length === 0 ? false : utils.isOneOf(o.description, appliedBlacklistedDistricts);
|
||||
return o.title != null && !isBlacklistedDistrict && titleNotBlacklisted && descNotBlacklisted;
|
||||
}
|
||||
|
||||
const config = {
|
||||
url: null,
|
||||
crawlContainer: '#srchrslt-adtable .ad-listitem ',
|
||||
//sort by date is standard oO
|
||||
sortByDateParam: null,
|
||||
waitForSelector: 'body',
|
||||
crawlFields: {
|
||||
id: '.aditem@data-adid | int',
|
||||
price: '.aditem-main--middle--price-shipping--price | removeNewline | trim',
|
||||
size: '.aditem-main .text-module-end | removeNewline | trim',
|
||||
title: '.aditem-main .text-module-begin a | removeNewline | trim',
|
||||
link: '.aditem-main .text-module-begin a@href | removeNewline | trim',
|
||||
description: '.aditem-main .aditem-main--middle--description | removeNewline | trim',
|
||||
address: '.aditem-main--top--left | trim | removeNewline',
|
||||
},
|
||||
normalize: normalize,
|
||||
filter: applyBlacklist,
|
||||
url: null,
|
||||
crawlContainer: '#srchrslt-adtable .ad-listitem ',
|
||||
//sort by date is standard oO
|
||||
sortByDateParam: null,
|
||||
waitForSelector: 'body',
|
||||
crawlFields: {
|
||||
id: '.aditem@data-adid | int',
|
||||
price: '.aditem-main--middle--price-shipping--price | removeNewline | trim',
|
||||
size: '.aditem-main .text-module-end | removeNewline | trim',
|
||||
title: '.aditem-main .text-module-begin a | removeNewline | trim',
|
||||
link: '.aditem-main .text-module-begin a@href | removeNewline | trim',
|
||||
description: '.aditem-main .aditem-main--middle--description | removeNewline | trim',
|
||||
address: '.aditem-main--top--left | trim | removeNewline',
|
||||
},
|
||||
normalize: normalize,
|
||||
filter: applyBlacklist,
|
||||
};
|
||||
export const metaInformation = {
|
||||
name: 'Ebay Kleinanzeigen',
|
||||
baseUrl: 'https://www.kleinanzeigen.de/',
|
||||
id: 'kleinanzeigen',
|
||||
name: 'Ebay Kleinanzeigen',
|
||||
baseUrl: 'https://www.kleinanzeigen.de/',
|
||||
id: 'kleinanzeigen',
|
||||
};
|
||||
export const init = (sourceConfig, blacklist, blacklistedDistricts) => {
|
||||
config.enabled = sourceConfig.enabled;
|
||||
config.url = sourceConfig.url;
|
||||
appliedBlacklistedDistricts = blacklistedDistricts || [];
|
||||
appliedBlackList = blacklist || [];
|
||||
config.enabled = sourceConfig.enabled;
|
||||
config.url = sourceConfig.url;
|
||||
appliedBlacklistedDistricts = blacklistedDistricts || [];
|
||||
appliedBlackList = blacklist || [];
|
||||
};
|
||||
export {config};
|
||||
export { config };
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
import utils, {buildHash} from '../utils.js';
|
||||
import utils, { buildHash } from '../utils.js';
|
||||
|
||||
let appliedBlackList = [];
|
||||
|
||||
function nullOrEmpty(val) {
|
||||
return val == null || val.length === 0;
|
||||
return val == null || val.length === 0;
|
||||
}
|
||||
|
||||
function normalize(o) {
|
||||
const link = nullOrEmpty(o.link) ? 'NO LINK' : `https://www.neubaukompass.de${o.link.substring(o.link.indexOf('/neubau'))}`;
|
||||
const id = buildHash(o.link, o.price);
|
||||
return Object.assign(o, {id, link});
|
||||
const link = nullOrEmpty(o.link)
|
||||
? 'NO LINK'
|
||||
: `https://www.neubaukompass.de${o.link.substring(o.link.indexOf('/neubau'))}`;
|
||||
const id = buildHash(o.link, o.price);
|
||||
return Object.assign(o, { id, link });
|
||||
}
|
||||
|
||||
function applyBlacklist(o) {
|
||||
return !utils.isOneOf(o.title, appliedBlackList);
|
||||
return !utils.isOneOf(o.title, appliedBlackList);
|
||||
}
|
||||
|
||||
const config = {
|
||||
url: null,
|
||||
crawlContainer: '.col-12.mb-4',
|
||||
sortByDateParam: 'Sortierung=Id&Richtung=DESC',
|
||||
waitForSelector: '.nbk-section',
|
||||
crawlFields: {
|
||||
id: 'a@href',
|
||||
title: 'a@title | removeNewline | trim',
|
||||
link: 'a@href',
|
||||
address: '.nbk-project-card__description | removeNewline | trim',
|
||||
price: '.nbk-project-card__spec-item .nbk-project-card__spec-value | removeNewline | trim',
|
||||
},
|
||||
normalize: normalize,
|
||||
filter: applyBlacklist,
|
||||
url: null,
|
||||
crawlContainer: '.col-12.mb-4',
|
||||
sortByDateParam: 'Sortierung=Id&Richtung=DESC',
|
||||
waitForSelector: '.nbk-section',
|
||||
crawlFields: {
|
||||
id: 'a@href',
|
||||
title: 'a@title | removeNewline | trim',
|
||||
link: 'a@href',
|
||||
address: '.nbk-project-card__description | removeNewline | trim',
|
||||
price: '.nbk-project-card__spec-item .nbk-project-card__spec-value | removeNewline | trim',
|
||||
},
|
||||
normalize: normalize,
|
||||
filter: applyBlacklist,
|
||||
};
|
||||
export const init = (sourceConfig, blacklist) => {
|
||||
config.enabled = sourceConfig.enabled;
|
||||
config.url = sourceConfig.url;
|
||||
appliedBlackList = blacklist || [];
|
||||
config.enabled = sourceConfig.enabled;
|
||||
config.url = sourceConfig.url;
|
||||
appliedBlackList = blacklist || [];
|
||||
};
|
||||
export const metaInformation = {
|
||||
name: 'Neubau Kompass',
|
||||
baseUrl: 'https://www.neubaukompass.de/',
|
||||
id: 'neubauKompass',
|
||||
name: 'Neubau Kompass',
|
||||
baseUrl: 'https://www.neubaukompass.de/',
|
||||
id: 'neubauKompass',
|
||||
};
|
||||
export {config};
|
||||
export { config };
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import utils, {buildHash} from '../utils.js';
|
||||
import utils, { buildHash } from '../utils.js';
|
||||
|
||||
let appliedBlackList = [];
|
||||
|
||||
function normalize(o) {
|
||||
const id = buildHash(o.id, o.price);
|
||||
const link = `https://www.wg-gesucht.de${o.link}`;
|
||||
return Object.assign(o, { id, link });
|
||||
const id = buildHash(o.id, o.price);
|
||||
const link = `https://www.wg-gesucht.de${o.link}`;
|
||||
return Object.assign(o, { id, link });
|
||||
}
|
||||
|
||||
function applyBlacklist(o) {
|
||||
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
||||
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
||||
return o.id != null && titleNotBlacklisted && descNotBlacklisted;
|
||||
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
||||
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
||||
return o.id != null && titleNotBlacklisted && descNotBlacklisted;
|
||||
}
|
||||
|
||||
const config = {
|
||||
url: null,
|
||||
crawlContainer: '#main_column .wgg_card',
|
||||
sortByDateParam: 'sort_column=0&sort_order=0',
|
||||
waitForSelector: 'body',
|
||||
crawlFields: {
|
||||
id: '@data-id',
|
||||
details: '.row .noprint .col-xs-11 |removeNewline |trim',
|
||||
price: '.middle .col-xs-3 |removeNewline |trim',
|
||||
size: '.middle .text-right |removeNewline |trim',
|
||||
title: '.truncate_title a |removeNewline |trim',
|
||||
link: '.truncate_title a@href',
|
||||
},
|
||||
normalize: normalize,
|
||||
filter: applyBlacklist,
|
||||
url: null,
|
||||
crawlContainer: '#main_column .wgg_card',
|
||||
sortByDateParam: 'sort_column=0&sort_order=0',
|
||||
waitForSelector: 'body',
|
||||
crawlFields: {
|
||||
id: '@data-id',
|
||||
details: '.row .noprint .col-xs-11 |removeNewline |trim',
|
||||
price: '.middle .col-xs-3 |removeNewline |trim',
|
||||
size: '.middle .text-right |removeNewline |trim',
|
||||
title: '.truncate_title a |removeNewline |trim',
|
||||
link: '.truncate_title a@href',
|
||||
},
|
||||
normalize: normalize,
|
||||
filter: applyBlacklist,
|
||||
};
|
||||
export const init = (sourceConfig, blacklist) => {
|
||||
config.enabled = sourceConfig.enabled;
|
||||
config.url = sourceConfig.url;
|
||||
appliedBlackList = blacklist || [];
|
||||
config.enabled = sourceConfig.enabled;
|
||||
config.url = sourceConfig.url;
|
||||
appliedBlackList = blacklist || [];
|
||||
};
|
||||
export const metaInformation = {
|
||||
name: 'Wg gesucht',
|
||||
baseUrl: 'https://www.wg-gesucht.de/',
|
||||
id: 'wgGesucht',
|
||||
name: 'Wg gesucht',
|
||||
baseUrl: 'https://www.wg-gesucht.de/',
|
||||
id: 'wgGesucht',
|
||||
};
|
||||
export {config};
|
||||
export { config };
|
||||
|
||||
2
prod.js
2
prod.js
@@ -1,2 +1,2 @@
|
||||
process.env.NODE_ENV = 'production';
|
||||
import('./index.js');
|
||||
import('./index.js');
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||
import {get} from '../mocks/mockNotification.js';
|
||||
import {mockFredy, providerConfig} from '../utils.js';
|
||||
import {expect} from 'chai';
|
||||
import { get } from '../mocks/mockNotification.js';
|
||||
import { mockFredy, providerConfig } from '../utils.js';
|
||||
import { expect } from 'chai';
|
||||
import * as provider from '../../lib/provider/immonet.js';
|
||||
|
||||
describe('#immonet testsuite()', () => {
|
||||
after(() => {
|
||||
similarityCache.stopCacheCleanup();
|
||||
});
|
||||
provider.init(providerConfig.immonet, [], []);
|
||||
it('should test immonet provider', async () => {
|
||||
const Fredy = await mockFredy();
|
||||
return await new Promise((resolve) => {
|
||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'immonet', similarityCache);
|
||||
fredy.execute().then((listing) => {
|
||||
expect(listing).to.be.a('array');
|
||||
const notificationObj = get();
|
||||
expect(notificationObj).to.be.a('object');
|
||||
expect(notificationObj.serviceName).to.equal('immonet');
|
||||
notificationObj.payload.forEach((notify) => {
|
||||
/** check the actual structure **/
|
||||
expect(notify.id).to.be.a('string');
|
||||
expect(notify.price).to.be.a('string');
|
||||
expect(notify.size).to.be.a('string');
|
||||
expect(notify.title).to.be.a('string');
|
||||
expect(notify.link).to.be.a('string');
|
||||
expect(notify.address).to.be.a('string');
|
||||
after(() => {
|
||||
similarityCache.stopCacheCleanup();
|
||||
});
|
||||
provider.init(providerConfig.immonet, [], []);
|
||||
it('should test immonet provider', async () => {
|
||||
const Fredy = await mockFredy();
|
||||
return await new Promise((resolve) => {
|
||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'immonet', similarityCache);
|
||||
fredy.execute().then((listing) => {
|
||||
expect(listing).to.be.a('array');
|
||||
const notificationObj = get();
|
||||
expect(notificationObj).to.be.a('object');
|
||||
expect(notificationObj.serviceName).to.equal('immonet');
|
||||
notificationObj.payload.forEach((notify) => {
|
||||
/** check the actual structure **/
|
||||
expect(notify.id).to.be.a('string');
|
||||
expect(notify.price).to.be.a('string');
|
||||
expect(notify.size).to.be.a('string');
|
||||
expect(notify.title).to.be.a('string');
|
||||
expect(notify.link).to.be.a('string');
|
||||
expect(notify.address).to.be.a('string');
|
||||
|
||||
expect(notify.size).that.does.include('m²');
|
||||
expect(notify.title).to.be.not.empty;
|
||||
expect(notify.address).to.be.not.empty;
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
expect(notify.size).that.does.include('m²');
|
||||
expect(notify.title).to.be.not.empty;
|
||||
expect(notify.address).to.be.not.empty;
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||
import {get} from '../mocks/mockNotification.js';
|
||||
import {mockFredy, providerConfig} from '../utils.js';
|
||||
import {expect} from 'chai';
|
||||
import { get } from '../mocks/mockNotification.js';
|
||||
import { mockFredy, providerConfig } from '../utils.js';
|
||||
import { expect } from 'chai';
|
||||
import * as provider from '../../lib/provider/neubauKompass.js';
|
||||
|
||||
describe('#neubauKompass testsuite()', () => {
|
||||
after(() => {
|
||||
similarityCache.stopCacheCleanup();
|
||||
});
|
||||
provider.init(providerConfig.neubauKompass, [], []);
|
||||
it('should test neubauKompass provider', async () => {
|
||||
const Fredy = await mockFredy();
|
||||
return await new Promise((resolve) => {
|
||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'neubauKompass', similarityCache);
|
||||
fredy.execute().then((listing) => {
|
||||
expect(listing).to.be.a('array');
|
||||
const notificationObj = get();
|
||||
expect(notificationObj.serviceName).to.equal('neubauKompass');
|
||||
notificationObj.payload.forEach((notify) => {
|
||||
expect(notify).to.be.a('object');
|
||||
/** check the actual structure **/
|
||||
expect(notify.id).to.be.a('string');
|
||||
expect(notify.title).to.be.a('string');
|
||||
expect(notify.link).to.be.a('string');
|
||||
expect(notify.address).to.be.a('string');
|
||||
/** check the values if possible **/
|
||||
expect(notify.title).to.be.not.empty;
|
||||
expect(notify.link).that.does.include('https://www.neubaukompass.de');
|
||||
expect(notify.address).to.be.not.empty;
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
after(() => {
|
||||
similarityCache.stopCacheCleanup();
|
||||
});
|
||||
provider.init(providerConfig.neubauKompass, [], []);
|
||||
it('should test neubauKompass provider', async () => {
|
||||
const Fredy = await mockFredy();
|
||||
return await new Promise((resolve) => {
|
||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'neubauKompass', similarityCache);
|
||||
fredy.execute().then((listing) => {
|
||||
expect(listing).to.be.a('array');
|
||||
const notificationObj = get();
|
||||
expect(notificationObj.serviceName).to.equal('neubauKompass');
|
||||
notificationObj.payload.forEach((notify) => {
|
||||
expect(notify).to.be.a('object');
|
||||
/** check the actual structure **/
|
||||
expect(notify.id).to.be.a('string');
|
||||
expect(notify.title).to.be.a('string');
|
||||
expect(notify.link).to.be.a('string');
|
||||
expect(notify.address).to.be.a('string');
|
||||
/** check the values if possible **/
|
||||
expect(notify.title).to.be.not.empty;
|
||||
expect(notify.link).that.does.include('https://www.neubaukompass.de');
|
||||
expect(notify.address).to.be.not.empty;
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { expect } from 'chai';
|
||||
import {buildHash} from '../../lib/utils.js';
|
||||
import { buildHash } from '../../lib/utils.js';
|
||||
|
||||
describe('utilsCheck', () => {
|
||||
describe('#utilsCheck()', () => {
|
||||
it('should be null when null input', () => {
|
||||
expect(buildHash(null)).to.be.null;
|
||||
});
|
||||
it('should be null when null empty', () => {
|
||||
expect(buildHash('')).to.be.null;
|
||||
});
|
||||
it('should return a value', () => {
|
||||
expect(buildHash('bla', '', null)).to.be.a.string;
|
||||
});
|
||||
it('should be null when null input', () => {
|
||||
expect(buildHash(null)).to.be.null;
|
||||
});
|
||||
it('should be null when null empty', () => {
|
||||
expect(buildHash('')).to.be.null;
|
||||
});
|
||||
it('should return a value', () => {
|
||||
expect(buildHash('bla', '', null)).to.be.a.string;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
168
ui/src/App.jsx
168
ui/src/App.jsx
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import InsufficientPermission from './components/permission/InsufficientPermission';
|
||||
import PermissionAwareRoute from './components/permission/PermissionAwareRoute';
|
||||
@@ -6,106 +6,108 @@ import GeneralSettings from './views/generalSettings/GeneralSettings';
|
||||
import JobMutation from './views/jobs/mutation/JobMutation';
|
||||
import UserMutator from './views/user/mutation/UserMutator';
|
||||
import JobInsight from './views/jobs/insights/JobInsight.jsx';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {Switch, Redirect} from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Switch, Redirect } from 'react-router-dom';
|
||||
import Logout from './components/logout/Logout';
|
||||
import Logo from './components/logo/Logo';
|
||||
import Menu from './components/menu/Menu';
|
||||
import Login from './views/login/Login';
|
||||
import Users from './views/user/Users';
|
||||
import Jobs from './views/jobs/Jobs';
|
||||
import {Route} from 'react-router';
|
||||
import { Route } from 'react-router';
|
||||
|
||||
import './App.less';
|
||||
import TrackingModal from './components/tracking/TrackingModal.jsx';
|
||||
import {Banner} from '@douyinfe/semi-ui';
|
||||
import { Banner } from '@douyinfe/semi-ui';
|
||||
|
||||
export default function FredyApp() {
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const currentUser = useSelector((state) => state.user.currentUser);
|
||||
const settings = useSelector((state) => state.generalSettings.settings);
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const currentUser = useSelector((state) => state.user.currentUser);
|
||||
const settings = useSelector((state) => state.generalSettings.settings);
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
await dispatch.user.getCurrentUser();
|
||||
if (!needsLogin()) {
|
||||
await dispatch.provider.getProvider();
|
||||
await dispatch.jobs.getJobs();
|
||||
await dispatch.jobs.getProcessingTimes();
|
||||
await dispatch.notificationAdapter.getAdapter();
|
||||
await dispatch.generalSettings.getGeneralSettings();
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
await dispatch.user.getCurrentUser();
|
||||
if (!needsLogin()) {
|
||||
await dispatch.provider.getProvider();
|
||||
await dispatch.jobs.getJobs();
|
||||
await dispatch.jobs.getProcessingTimes();
|
||||
await dispatch.notificationAdapter.getAdapter();
|
||||
await dispatch.generalSettings.getGeneralSettings();
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
init();
|
||||
}, [currentUser?.userId]);
|
||||
init();
|
||||
}, [currentUser?.userId]);
|
||||
|
||||
const needsLogin = () => {
|
||||
return currentUser == null || Object.keys(currentUser).length === 0;
|
||||
};
|
||||
const needsLogin = () => {
|
||||
return currentUser == null || Object.keys(currentUser).length === 0;
|
||||
};
|
||||
|
||||
const isAdmin = () => currentUser != null && currentUser.isAdmin;
|
||||
const isAdmin = () => currentUser != null && currentUser.isAdmin;
|
||||
|
||||
const login = () => (
|
||||
const login = () => (
|
||||
<Switch>
|
||||
<Route name="Login" path={'/login'} component={Login} />
|
||||
<Redirect from="*" to={'/login'} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
return loading ? null : needsLogin() ? (
|
||||
login()
|
||||
) : (
|
||||
<div className="app">
|
||||
<div className="app__container">
|
||||
<Logout />
|
||||
<Logo width={190} white />
|
||||
<Menu isAdmin={isAdmin()} />
|
||||
|
||||
{settings.demoMode && (
|
||||
<>
|
||||
<Banner
|
||||
fullMode={true}
|
||||
type="info"
|
||||
bordered
|
||||
closeIcon={null}
|
||||
description="You're currently viewing the demo version of Fredy. Jobs won't scrape websites, and any changes you make will be reverted at midnight."
|
||||
/>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{settings.analyticsEnabled === null && !settings.demoMode && <TrackingModal />}
|
||||
<Switch>
|
||||
<Route name="Login" path={'/login'} component={Login}/>
|
||||
<Redirect from="*" to={'/login'}/>
|
||||
<Route name="Insufficient Permission" path={'/403'} component={InsufficientPermission} />
|
||||
<Route name="Create new Job" path={'/jobs/new'} component={JobMutation} />
|
||||
<Route name="Edit a Job" path={'/jobs/edit/:jobId'} component={JobMutation} />
|
||||
<Route name="Insights into a Job" path={'/jobs/insights/:jobId'} component={JobInsight} />
|
||||
<Route name="Job overview" path={'/jobs'} component={Jobs} />
|
||||
<PermissionAwareRoute
|
||||
name="Create new User"
|
||||
path="/users/new"
|
||||
component={<UserMutator />}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
<PermissionAwareRoute
|
||||
name="Edit a user"
|
||||
path="/users/edit/:userId"
|
||||
component={<UserMutator />}
|
||||
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>
|
||||
);
|
||||
|
||||
return loading ? null : needsLogin() ? (
|
||||
login()
|
||||
) : (
|
||||
<div className="app">
|
||||
<div className="app__container">
|
||||
<Logout/>
|
||||
<Logo width={190} white/>
|
||||
<Menu isAdmin={isAdmin()}/>
|
||||
|
||||
{settings.demoMode && (
|
||||
<>
|
||||
<Banner fullMode={true}
|
||||
type="info"
|
||||
bordered
|
||||
closeIcon={null}
|
||||
description="You're currently viewing the demo version of Fredy. Jobs won't scrape websites, and any changes you make will be reverted at midnight."
|
||||
/>
|
||||
<br/>
|
||||
</>)}
|
||||
{(settings.analyticsEnabled === null && !settings.demoMode) && <TrackingModal/>}
|
||||
<Switch>
|
||||
<Route name="Insufficient Permission" path={'/403'} component={InsufficientPermission}/>
|
||||
<Route name="Create new Job" path={'/jobs/new'} component={JobMutation}/>
|
||||
<Route name="Edit a Job" path={'/jobs/edit/:jobId'} component={JobMutation}/>
|
||||
<Route name="Insights into a Job" path={'/jobs/insights/:jobId'} component={JobInsight}/>
|
||||
<Route name="Job overview" path={'/jobs'} component={Jobs}/>
|
||||
<PermissionAwareRoute
|
||||
name="Create new User"
|
||||
path="/users/new"
|
||||
component={<UserMutator/>}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
<PermissionAwareRoute
|
||||
name="Edit a user"
|
||||
path="/users/edit/:userId"
|
||||
component={<UserMutator/>}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FredyApp.displayName = 'FredyApp';
|
||||
|
||||
@@ -23,5 +23,5 @@ root.render(
|
||||
<App />
|
||||
</LocaleProvider>
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
@@ -1,52 +1,56 @@
|
||||
import React from 'react';
|
||||
import {Modal} from '@douyinfe/semi-ui';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
import Logo from '../logo/Logo.jsx';
|
||||
import {xhrPost} from '../../services/xhr.js';
|
||||
import { xhrPost } from '../../services/xhr.js';
|
||||
|
||||
import './TrackingModal.less';
|
||||
import inDevelopment from '../../services/developmentMode.js';
|
||||
|
||||
const saveResponse = async (analyticsEnabled) => {
|
||||
await xhrPost('/api/admin/generalSettings', {
|
||||
analyticsEnabled
|
||||
});
|
||||
await xhrPost('/api/admin/generalSettings', {
|
||||
analyticsEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
export default function TrackingModal() {
|
||||
if(inDevelopment()){
|
||||
return null;
|
||||
}
|
||||
if (inDevelopment()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Modal
|
||||
visible={true}
|
||||
onOk={async () => {
|
||||
await saveResponse(true);
|
||||
location.reload();
|
||||
}}
|
||||
onCancel={async () => {
|
||||
await saveResponse(false);
|
||||
location.reload();
|
||||
}}
|
||||
maskClosable={false}
|
||||
closable={false}
|
||||
okText="Yes! I want to help"
|
||||
cancelText="No, thanks"
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
onOk={async () => {
|
||||
await saveResponse(true);
|
||||
location.reload();
|
||||
}}
|
||||
onCancel={async () => {
|
||||
await saveResponse(false);
|
||||
location.reload();
|
||||
}}
|
||||
maskClosable={false}
|
||||
closable={false}
|
||||
okText="Yes! I want to help"
|
||||
cancelText="No, thanks"
|
||||
>
|
||||
<Logo white/>
|
||||
<div className="trackingModal__description">
|
||||
<p>Hey 👋</p>
|
||||
<p>Fed up with popups? Yeah, me too. But this one’s important, and I promise it will only appear once ;)</p>
|
||||
<p>Fredy is completely free (and will always remain free). If you’d like, you can support me by donating
|
||||
through my GitHub, but there’s absolutely no obligation to do so.</p>
|
||||
<p>However, it would be a huge
|
||||
help if you’d allow me to collect some analytical data. Wait, before you click "no", let me explain. If
|
||||
you
|
||||
agree, Fredy will send a ping to my Mixpanel project each time it runs.</p>
|
||||
<p>The data includes: names of
|
||||
active adapters/providers, OS, architecture, Node version, and language. The information is entirely
|
||||
anonymous and helps me understand which adapters/providers are most frequently used.</p>
|
||||
<p>Thanks🤘</p>
|
||||
</div>
|
||||
</Modal>;
|
||||
|
||||
}
|
||||
<Logo white />
|
||||
<div className="trackingModal__description">
|
||||
<p>Hey 👋</p>
|
||||
<p>Fed up with popups? Yeah, me too. But this one’s important, and I promise it will only appear once ;)</p>
|
||||
<p>
|
||||
Fredy is completely free (and will always remain free). If you’d like, you can support me by donating through
|
||||
my GitHub, but there’s absolutely no obligation to do so.
|
||||
</p>
|
||||
<p>
|
||||
However, it would be a huge help if you’d allow me to collect some analytical data. Wait, before you click
|
||||
"no", let me explain. If you agree, Fredy will send a ping to my Mixpanel project each time it runs.
|
||||
</p>
|
||||
<p>
|
||||
The data includes: names of active adapters/providers, OS, architecture, Node version, and language. The
|
||||
information is entirely anonymous and helps me understand which adapters/providers are most frequently used.
|
||||
</p>
|
||||
<p>Thanks🤘</p>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,261 +1,251 @@
|
||||
import React from 'react';
|
||||
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import {Divider, TimePicker, Button, Checkbox} from '@douyinfe/semi-ui';
|
||||
import {InputNumber} from '@douyinfe/semi-ui';
|
||||
import { Divider, TimePicker, Button, Checkbox } from '@douyinfe/semi-ui';
|
||||
import { InputNumber } from '@douyinfe/semi-ui';
|
||||
import Headline from '../../components/headline/Headline';
|
||||
import {xhrPost} from '../../services/xhr';
|
||||
import {SegmentPart} from '../../components/segment/SegmentPart';
|
||||
import {Banner, Toast} from '@douyinfe/semi-ui';
|
||||
import {IconSave, IconCalendar, IconRefresh, IconSignal, IconLineChartStroked, IconSearch} from '@douyinfe/semi-icons';
|
||||
import { xhrPost } from '../../services/xhr';
|
||||
import { SegmentPart } from '../../components/segment/SegmentPart';
|
||||
import { Banner, Toast } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconSave,
|
||||
IconCalendar,
|
||||
IconRefresh,
|
||||
IconSignal,
|
||||
IconLineChartStroked,
|
||||
IconSearch,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import './GeneralSettings.less';
|
||||
|
||||
function formatFromTimestamp(ts) {
|
||||
const date = new Date(ts);
|
||||
return `${date.getHours()}:${date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()}`;
|
||||
const date = new Date(ts);
|
||||
return `${date.getHours()}:${date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()}`;
|
||||
}
|
||||
|
||||
function formatFromTBackend(time) {
|
||||
if (time == null || time.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date();
|
||||
const split = time.split(':');
|
||||
date.setHours(split[0]);
|
||||
date.setMinutes(split[1]);
|
||||
return date.getTime();
|
||||
if (time == null || time.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date();
|
||||
const split = time.split(':');
|
||||
date.setHours(split[0]);
|
||||
date.setMinutes(split[1]);
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
const GeneralSettings = function GeneralSettings() {
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
|
||||
const settings = useSelector((state) => state.generalSettings.settings);
|
||||
const settings = useSelector((state) => state.generalSettings.settings);
|
||||
|
||||
const [interval, setInterval] = React.useState('');
|
||||
const [port, setPort] = React.useState('');
|
||||
const [workingHourFrom, setWorkingHourFrom] = React.useState(null);
|
||||
const [workingHourTo, setWorkingHourTo] = React.useState(null);
|
||||
const [demoMode, setDemoMode] = React.useState(null);
|
||||
const [analyticsEnabled, setAnalyticsEnabled] = React.useState(null);
|
||||
const [interval, setInterval] = React.useState('');
|
||||
const [port, setPort] = React.useState('');
|
||||
const [workingHourFrom, setWorkingHourFrom] = React.useState(null);
|
||||
const [workingHourTo, setWorkingHourTo] = React.useState(null);
|
||||
const [demoMode, setDemoMode] = React.useState(null);
|
||||
const [analyticsEnabled, setAnalyticsEnabled] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
async function init() {
|
||||
await dispatch.generalSettings.getGeneralSettings();
|
||||
setLoading(false);
|
||||
}
|
||||
React.useEffect(() => {
|
||||
async function init() {
|
||||
await dispatch.generalSettings.getGeneralSettings();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
init();
|
||||
}, []);
|
||||
init();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
async function init() {
|
||||
setInterval(settings?.interval);
|
||||
setPort(settings?.port);
|
||||
setWorkingHourFrom(settings?.workingHours?.from);
|
||||
setWorkingHourTo(settings?.workingHours?.to);
|
||||
setAnalyticsEnabled(settings?.analyticsEnabled || false);
|
||||
setDemoMode(settings?.demoMode || false);
|
||||
}
|
||||
React.useEffect(() => {
|
||||
async function init() {
|
||||
setInterval(settings?.interval);
|
||||
setPort(settings?.port);
|
||||
setWorkingHourFrom(settings?.workingHours?.from);
|
||||
setWorkingHourTo(settings?.workingHours?.to);
|
||||
setAnalyticsEnabled(settings?.analyticsEnabled || false);
|
||||
setDemoMode(settings?.demoMode || false);
|
||||
}
|
||||
|
||||
init();
|
||||
}, [settings]);
|
||||
init();
|
||||
}, [settings]);
|
||||
|
||||
const nullOrEmpty = (val) => val == null || val.length === 0;
|
||||
const nullOrEmpty = (val) => val == null || val.length === 0;
|
||||
|
||||
const throwMessage = (message, type) => {
|
||||
if (type === 'error') {
|
||||
Toast.error(message);
|
||||
} else {
|
||||
Toast.success(message);
|
||||
}
|
||||
};
|
||||
const throwMessage = (message, type) => {
|
||||
if (type === 'error') {
|
||||
Toast.error(message);
|
||||
} else {
|
||||
Toast.success(message);
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
workingHours: {
|
||||
from: workingHourFrom,
|
||||
to: workingHourTo,
|
||||
},
|
||||
demoMode,
|
||||
analyticsEnabled
|
||||
});
|
||||
} catch (exception) {
|
||||
console.error(exception);
|
||||
if(exception?.json?.message != null){
|
||||
throwMessage(exception.json.message, 'error');
|
||||
}else {
|
||||
throwMessage('Error while trying to store settings.', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
throwMessage('Settings stored successfully. We will reload your browser in 3 seconds.', 'success');
|
||||
setTimeout(()=>{
|
||||
location.reload();
|
||||
}, 3000);
|
||||
};
|
||||
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,
|
||||
workingHours: {
|
||||
from: workingHourFrom,
|
||||
to: workingHourTo,
|
||||
},
|
||||
demoMode,
|
||||
analyticsEnabled,
|
||||
});
|
||||
} catch (exception) {
|
||||
console.error(exception);
|
||||
if (exception?.json?.message != null) {
|
||||
throwMessage(exception.json.message, 'error');
|
||||
} else {
|
||||
throwMessage('Error while trying to store settings.', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
throwMessage('Settings stored successfully. We will reload your browser in 3 seconds.', 'success');
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!loading && (
|
||||
<React.Fragment>
|
||||
<Headline text="General Settings"/>
|
||||
<div>
|
||||
<SegmentPart
|
||||
name="Interval"
|
||||
helpText="Interval in minutes for running queries against the configured services."
|
||||
Icon={IconRefresh}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1440}
|
||||
placeholder="Interval in minutes"
|
||||
value={interval}
|
||||
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
||||
onChange={(value) => setInterval(value)}
|
||||
suffix={'minutes'}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem"/>
|
||||
<SegmentPart name="Port" helpText="Port on which Fredy is running." Icon={IconSignal}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={99999}
|
||||
placeholder="Port"
|
||||
value={port}
|
||||
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
||||
onChange={(value) => setPort(value)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem"/>
|
||||
<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={IconCalendar}
|
||||
>
|
||||
<div className="generalSettings__timePickerContainer">
|
||||
<TimePicker
|
||||
format={'HH:mm'}
|
||||
insetLabel="From"
|
||||
value={formatFromTBackend(workingHourFrom)}
|
||||
placeholder=""
|
||||
onChange={(val) => {
|
||||
setWorkingHourFrom(val == null ? null : formatFromTimestamp(val));
|
||||
}}
|
||||
/>
|
||||
<TimePicker
|
||||
format={'HH:mm'}
|
||||
insetLabel="Until"
|
||||
value={formatFromTBackend(workingHourTo)}
|
||||
placeholder=""
|
||||
onChange={(val) => {
|
||||
setWorkingHourTo(val == null ? null : formatFromTimestamp(val));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem"/>
|
||||
return (
|
||||
<div>
|
||||
{!loading && (
|
||||
<React.Fragment>
|
||||
<Headline text="General Settings" />
|
||||
<div>
|
||||
<SegmentPart
|
||||
name="Interval"
|
||||
helpText="Interval in minutes for running queries against the configured services."
|
||||
Icon={IconRefresh}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1440}
|
||||
placeholder="Interval in minutes"
|
||||
value={interval}
|
||||
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
||||
onChange={(value) => setInterval(value)}
|
||||
suffix={'minutes'}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart name="Port" helpText="Port on which Fredy is running." Icon={IconSignal}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={99999}
|
||||
placeholder="Port"
|
||||
value={port}
|
||||
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
||||
onChange={(value) => setPort(value)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<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={IconCalendar}
|
||||
>
|
||||
<div className="generalSettings__timePickerContainer">
|
||||
<TimePicker
|
||||
format={'HH:mm'}
|
||||
insetLabel="From"
|
||||
value={formatFromTBackend(workingHourFrom)}
|
||||
placeholder=""
|
||||
onChange={(val) => {
|
||||
setWorkingHourFrom(val == null ? null : formatFromTimestamp(val));
|
||||
}}
|
||||
/>
|
||||
<TimePicker
|
||||
format={'HH:mm'}
|
||||
insetLabel="Until"
|
||||
value={formatFromTBackend(workingHourTo)}
|
||||
placeholder=""
|
||||
onChange={(val) => {
|
||||
setWorkingHourTo(val == null ? null : formatFromTimestamp(val));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
|
||||
<SegmentPart
|
||||
name="Analytics"
|
||||
helpText="Insights into the usage of Fredy."
|
||||
Icon={IconLineChartStroked}
|
||||
>
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type="info"
|
||||
closeIcon={null}
|
||||
title={
|
||||
<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>
|
||||
Explanation
|
||||
</div>
|
||||
}
|
||||
style={{marginBottom: '1rem'}}
|
||||
description={
|
||||
<div>
|
||||
Analytics are disabled by default. If you choose to enable them, we will begin tracking the following:<br/>
|
||||
<ul>
|
||||
<li>Name of active provider (e.g. Immoscout)</li>
|
||||
<li>Name of active adapter (e.g. Console)</li>
|
||||
<li>language</li>
|
||||
<li>os</li>
|
||||
<li>node version</li>
|
||||
<li>arch</li>
|
||||
</ul>
|
||||
The data is sent anonymously and helps me understand which providers or adapters are being used the most. In the end it helps me to improve fredy.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<SegmentPart name="Analytics" helpText="Insights into the usage of Fredy." Icon={IconLineChartStroked}>
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type="info"
|
||||
closeIcon={null}
|
||||
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Explanation</div>}
|
||||
style={{ marginBottom: '1rem' }}
|
||||
description={
|
||||
<div>
|
||||
Analytics are disabled by default. If you choose to enable them, we will begin tracking the
|
||||
following:
|
||||
<br />
|
||||
<ul>
|
||||
<li>Name of active provider (e.g. Immoscout)</li>
|
||||
<li>Name of active adapter (e.g. Console)</li>
|
||||
<li>language</li>
|
||||
<li>os</li>
|
||||
<li>node version</li>
|
||||
<li>arch</li>
|
||||
</ul>
|
||||
The data is sent anonymously and helps me understand which providers or adapters are being used the
|
||||
most. In the end it helps me to improve fredy.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={analyticsEnabled}
|
||||
onChange={(e) => setAnalyticsEnabled(e.target.checked)}
|
||||
> Enabled
|
||||
</Checkbox>
|
||||
<Checkbox checked={analyticsEnabled} onChange={(e) => setAnalyticsEnabled(e.target.checked)}>
|
||||
{' '}
|
||||
Enabled
|
||||
</Checkbox>
|
||||
</SegmentPart>
|
||||
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
|
||||
<Divider margin="1rem"/>
|
||||
<SegmentPart name="Demo Mode" helpText="If enabled, Fredy runs in demo mode." Icon={IconSearch}>
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type="info"
|
||||
closeIcon={null}
|
||||
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Explanation</div>}
|
||||
style={{ marginBottom: '1rem' }}
|
||||
description={
|
||||
<div>
|
||||
In demo mode, Fredy will not (really) search for any real estates. Fredy is in a lockdown mode. Also
|
||||
all database files will be set back to the default values at midnight.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<SegmentPart
|
||||
name="Demo Mode"
|
||||
helpText="If enabled, Fredy runs in demo mode."
|
||||
Icon={IconSearch}
|
||||
>
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type="info"
|
||||
closeIcon={null}
|
||||
title={
|
||||
<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>
|
||||
Explanation
|
||||
</div>
|
||||
}
|
||||
style={{marginBottom: '1rem'}}
|
||||
description={
|
||||
<div>
|
||||
In demo mode, Fredy will not (really) search for any real estates. Fredy is in a lockdown mode. Also
|
||||
all database files will be set back to the default values at midnight.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Checkbox checked={demoMode} onChange={(e) => setDemoMode(e.target.checked)}>
|
||||
{' '}
|
||||
Enabled
|
||||
</Checkbox>
|
||||
</SegmentPart>
|
||||
|
||||
<Checkbox
|
||||
checked={demoMode}
|
||||
onChange={(e) => setDemoMode(e.target.checked)}
|
||||
> Enabled
|
||||
</Checkbox>
|
||||
|
||||
</SegmentPart>
|
||||
|
||||
<Divider margin="1rem"/>
|
||||
<Button type="primary" theme="solid" onClick={onStore} icon={<IconSave/>}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
<Divider margin="1rem" />
|
||||
<Button type="primary" theme="solid" onClick={onStore} icon={<IconSave />}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralSettings;
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import React from 'react';
|
||||
import {format} from '../../services/time/timeService';
|
||||
import {Descriptions} from '@douyinfe/semi-ui';
|
||||
import { format } from '../../services/time/timeService';
|
||||
import { Descriptions } from '@douyinfe/semi-ui';
|
||||
|
||||
export default function ProcessingTimes({processingTimes = {}}) {
|
||||
if (Object.keys(processingTimes).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Descriptions
|
||||
row
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#35363c',
|
||||
borderRadius: '4px',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<Descriptions.Item itemKey="Processing Interval">{processingTimes.interval} min</Descriptions.Item>
|
||||
{processingTimes.lastRun && (
|
||||
<>
|
||||
<Descriptions.Item itemKey="Last run">{format(processingTimes.lastRun)}</Descriptions.Item>
|
||||
<Descriptions.Item itemKey="Next run">
|
||||
{format(processingTimes.lastRun + processingTimes.interval * 60000)}
|
||||
</Descriptions.Item>
|
||||
</>
|
||||
)}
|
||||
</Descriptions>
|
||||
</>
|
||||
);
|
||||
export default function ProcessingTimes({ processingTimes = {} }) {
|
||||
if (Object.keys(processingTimes).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Descriptions
|
||||
row
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#35363c',
|
||||
borderRadius: '4px',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<Descriptions.Item itemKey="Processing Interval">{processingTimes.interval} min</Descriptions.Item>
|
||||
{processingTimes.lastRun && (
|
||||
<>
|
||||
<Descriptions.Item itemKey="Last run">{format(processingTimes.lastRun)}</Descriptions.Item>
|
||||
<Descriptions.Item itemKey="Next run">
|
||||
{format(processingTimes.lastRun + processingTimes.interval * 60000)}
|
||||
</Descriptions.Item>
|
||||
</>
|
||||
)}
|
||||
</Descriptions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const validate = (selectedAdapter) => {
|
||||
}
|
||||
if (uiElement.type === 'number') {
|
||||
const numberValue = parseFloat(uiElement.value);
|
||||
if(isNaN(numberValue) || numberValue < 0) {
|
||||
if (isNaN(numberValue) || numberValue < 0) {
|
||||
results.push('A number field cannot contain anything else and must be > 0.');
|
||||
continue;
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export default function NotificationAdapterMutator({
|
||||
id: selectedAdapter.id,
|
||||
name: selectedAdapter.name,
|
||||
fields: selectedAdapter.fields || {},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
setSelectedAdapter(null);
|
||||
@@ -114,7 +114,7 @@ export default function NotificationAdapterMutator({
|
||||
setSuccessMessage('It seems like it worked! Please check your service.');
|
||||
})
|
||||
.catch((error) =>
|
||||
setValidationMessage(`This did not work :-( I've received the following error: ${error.json.message}`)
|
||||
setValidationMessage(`This did not work :-( I've received the following error: ${error.json.message}`),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -229,7 +229,7 @@ export default function NotificationAdapterMutator({
|
||||
.filter((option) =>
|
||||
editNotificationAdapter != null
|
||||
? true
|
||||
: selected.find((selectedOption) => selectedOption.id === option.key) == null
|
||||
: selected.find((selectedOption) => selectedOption.id === option.key) == null,
|
||||
)
|
||||
.sort(sortAdapter)}
|
||||
onChange={(value) => {
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function ProviderMutator({ onVisibilityChanged, visible = false,
|
||||
url: providerUrl,
|
||||
id: selectedProvider.id,
|
||||
name: selectedProvider.name,
|
||||
})
|
||||
}),
|
||||
);
|
||||
setProviderUrl(null);
|
||||
setSelectedProvider(null);
|
||||
|
||||
@@ -1,102 +1,105 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import cityBackground from '../../assets/city_background.jpg';
|
||||
import Logo from '../../components/logo/Logo';
|
||||
import {xhrPost} from '../../services/xhr';
|
||||
import {useHistory} from 'react-router';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {Input, Button, Banner} from '@douyinfe/semi-ui';
|
||||
import { xhrPost } from '../../services/xhr';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Input, Button, Banner } from '@douyinfe/semi-ui';
|
||||
|
||||
import './login.less';
|
||||
import {IconUser, IconLock} from '@douyinfe/semi-icons';
|
||||
import { IconUser, IconLock } from '@douyinfe/semi-icons';
|
||||
|
||||
export default function Login() {
|
||||
const dispatch = useDispatch();
|
||||
const [username, setUserName] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [error, setError] = React.useState(null);
|
||||
const demoMode = useSelector((state) => state.demoMode.demoMode || false);
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const [username, setUserName] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [error, setError] = React.useState(null);
|
||||
const demoMode = useSelector((state) => state.demoMode.demoMode || false);
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
await dispatch.demoMode.getDemoMode();
|
||||
}
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
await dispatch.demoMode.getDemoMode();
|
||||
}
|
||||
|
||||
init();
|
||||
}, []);
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const tryLogin = async () => {
|
||||
if (username.length === 0 || password.length === 0) {
|
||||
setError('Username and password are mandatory.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await xhrPost('/api/login', {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
setError(null);
|
||||
} catch (Exception) {
|
||||
setError('Login not successful...');
|
||||
return;
|
||||
}
|
||||
await dispatch.user.getCurrentUser();
|
||||
history.push('/jobs');
|
||||
};
|
||||
const tryLogin = async () => {
|
||||
if (username.length === 0 || password.length === 0) {
|
||||
setError('Username and password are mandatory.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await xhrPost('/api/login', {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
setError(null);
|
||||
} catch (Exception) {
|
||||
setError('Login not successful...');
|
||||
return;
|
||||
}
|
||||
await dispatch.user.getCurrentUser();
|
||||
history.push('/jobs');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login">
|
||||
<div className="login__bgImage" style={{background: `url("${cityBackground}")`}}/>
|
||||
<Logo/>
|
||||
<form>
|
||||
<div className="login__loginWrapper">
|
||||
{error && <Banner type="danger" closeIcon={null} description={error}/>}
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<IconUser/>}
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
showClear
|
||||
style={{marginTop: error ? '1rem' : '4rem'}}
|
||||
autoFocus
|
||||
onChange={(value) => setUserName(value)}
|
||||
onKeyPress={async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
await tryLogin();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
return (
|
||||
<div className="login">
|
||||
<div className="login__bgImage" style={{ background: `url("${cityBackground}")` }} />
|
||||
<Logo />
|
||||
<form>
|
||||
<div className="login__loginWrapper">
|
||||
{error && <Banner type="danger" closeIcon={null} description={error} />}
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<IconUser />}
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
showClear
|
||||
style={{ marginTop: error ? '1rem' : '4rem' }}
|
||||
autoFocus
|
||||
onChange={(value) => setUserName(value)}
|
||||
onKeyPress={async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
await tryLogin();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
size="large"
|
||||
mode="password"
|
||||
prefix={<IconLock/>}
|
||||
value={password}
|
||||
placeholder="Password"
|
||||
style={{marginTop: '2rem'}}
|
||||
onChange={(value) => setPassword(value)}
|
||||
onKeyPress={async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
await tryLogin();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
size="large"
|
||||
mode="password"
|
||||
prefix={<IconLock />}
|
||||
value={password}
|
||||
placeholder="Password"
|
||||
style={{ marginTop: '2rem' }}
|
||||
onChange={(value) => setPassword(value)}
|
||||
onKeyPress={async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
await tryLogin();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type="primary" onClick={tryLogin} theme="solid" style={{marginTop: '3rem'}}>
|
||||
Login
|
||||
</Button>
|
||||
<br/>
|
||||
{demoMode && <Banner fullMode={true}
|
||||
type="info"
|
||||
bordered
|
||||
closeIcon={null}
|
||||
description="This is the demo version of Fredy. Use 'demo' as both the username and password to log in."
|
||||
/>}
|
||||
</div>
|
||||
</form>
|
||||
<Button type="primary" onClick={tryLogin} theme="solid" style={{ marginTop: '3rem' }}>
|
||||
Login
|
||||
</Button>
|
||||
<br />
|
||||
{demoMode && (
|
||||
<Banner
|
||||
fullMode={true}
|
||||
type="info"
|
||||
bordered
|
||||
closeIcon={null}
|
||||
description="This is the demo version of Fredy. Use 'demo' as both the username and password to log in."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Login.displayName = 'Login';
|
||||
|
||||
Reference in New Issue
Block a user