Compare commits

...

8 Commits

Author SHA1 Message Date
Christian Kellner
f8f911aa00 improving tracking 2024-12-03 14:05:00 +01:00
Christian Kellner
13b8701447 Update CONTRIBUTING.md 2024-12-02 15:02:36 +01:00
Christian Kellner
e25b956eda Update config.json 2024-11-22 12:32:37 +01:00
weakmap@gmail.com
a2c769f786 Merge branch 'master' of https://github.com/orangecoding/fredy 2024-11-22 11:37:51 +01:00
weakmap@gmail.com
1825a25eaa fixing typo 2024-11-22 11:37:44 +01:00
Christian Kellner
0f20b85f38 Update README.md 2024-11-22 09:38:50 +01:00
weakmap@gmail.com
d17ef9ef1e update fredy version 2024-11-22 09:11:43 +01:00
Christian Kellner
337ee922a6 Demo Mode (#117)
* Adding Demo Mode to Fredy
2024-11-22 09:11:10 +01:00
24 changed files with 526 additions and 289 deletions

View File

@@ -106,9 +106,7 @@ exports.config = {
``` ```
#### Running Tests #### Running Tests
If you've written a new provider you are an awesome person. You know it and I do. If you now write tests for it, you are even more awesome. And who doesn't want to be more awesome right? If you've written a new provider you are an awesome person. If you now write tests for it, you are even more awesome. And who doesn't want to be more awesome right?
To write tests for provider, you need to use Node 8 as the tests are using `async / await`
#### Codestyle #### Codestyle
I'm using Eslint to maintain quote style and quality. Do not skip it... I'm using Eslint to maintain quote style and quality. Do not skip it...

View File

@@ -15,6 +15,9 @@ If you like my work, consider becoming a sponsor. I'm not expecting anybody to p
_Fredy_ is supported by JetBrains under Open Source Support Program _Fredy_ is supported by JetBrains under Open Source Support Program
## Demo
If you want to try out _Fredy_, you can access the demo version [here](https://fredy.orange-coding.net) 🤘
## Usage ## Usage
- Make sure to use Node.js 20 or above - Make sure to use Node.js 20 or above

View File

@@ -1 +1 @@
{"interval":"60","port":9998,"scrapingAnt":{"apiKey":"","proxy":"datacenter"},"workingHours":{"from":"","to":""},"demoMode":false,"analyticsEnabled":null} {"interval":"60","port":9998,"scrapingAnt":{"apiKey":"","proxy":"datacenter"},"workingHours":{"from":"","to":""},"demoMode":false,"analyticsEnabled":null}

View File

@@ -1,5 +1,5 @@
import fs from 'fs'; 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 * as similarityCache from './lib/services/similarity-check/similarityCache.js';
import { setLastJobExecution } from './lib/services/storage/listingsStorage.js'; import { setLastJobExecution } from './lib/services/storage/listingsStorage.js';
import * as jobStorage from './lib/services/storage/jobStorage.js'; import * as jobStorage from './lib/services/storage/jobStorage.js';
@@ -7,6 +7,8 @@ import FredyRuntime from './lib/FredyRuntime.js';
import { duringWorkingHoursOrNotSet } from './lib/utils.js'; import { duringWorkingHoursOrNotSet } from './lib/utils.js';
import './lib/api/api.js'; import './lib/api/api.js';
import {track} from './lib/services/tracking/Tracker.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 db folder does not exist, ensure to create it before loading anything else
if (!fs.existsSync('./db')) { if (!fs.existsSync('./db')) {
fs.mkdirSync('./db'); fs.mkdirSync('./db');
@@ -17,35 +19,43 @@ const provider = fs.readdirSync(path).filter((file) => file.endsWith('.js'));
const INTERVAL = config.interval * 60 * 1000; const INTERVAL = config.interval * 60 * 1000;
/* eslint-disable no-console */ /* eslint-disable no-console */
console.log(`Started Fredy successfully. Ui can be accessed via http://localhost:${config.port}`); console.log(`Started Fredy successfully. Ui can be accessed via http://localhost:${config.port}`);
if(config.demoMode){
console.info('Running in demo mode');
cleanupDemoAtMidnight();
}
/* eslint-enable no-console */ /* eslint-enable no-console */
const fetchedProvider = await Promise.all( 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();
setInterval( setInterval(
(function exec() { (function exec() {
const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now()); const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now());
if (isDuringWorkingHoursOrNotSet) { if(!config.demoMode) {
track(); if (isDuringWorkingHoursOrNotSet) {
config.lastRun = Date.now(); track();
jobStorage config.lastRun = Date.now();
.getJobs() jobStorage
.filter((job) => job.enabled) .getJobs()
.forEach((job) => { .filter((job) => job.enabled)
job.provider .forEach((job) => {
.filter((p) => fetchedProvider.find((fp) => fp.metaInformation.id === p.id) != null) job.provider
.forEach(async (prov) => { .filter((p) => fetchedProvider.find((fp) => fp.metaInformation.id === p.id) != null)
const pro = fetchedProvider.find((fp) => fp.metaInformation.id === prov.id); .forEach(async (prov) => {
pro.init(prov, job.blacklist); const pro = fetchedProvider.find((fp) => fp.metaInformation.id === prov.id);
await new FredyRuntime(pro.config, job.notificationAdapter, prov.id, job.id, similarityCache).execute(); pro.init(prov, job.blacklist);
setLastJobExecution(job.id); await new FredyRuntime(pro.config, job.notificationAdapter, prov.id, job.id, similarityCache).execute();
}); setLastJobExecution(job.id);
}); });
} else { });
/* eslint-disable no-console */ } else {
console.debug('Working hours set. Skipping as outside of working hours.'); /* eslint-disable no-console */
/* eslint-enable no-console */ console.debug('Working hours set. Skipping as outside of working hours.');
} /* eslint-enable no-console */
}
}
return exec; return exec;
})(), })(),
INTERVAL INTERVAL

View File

@@ -12,6 +12,7 @@ import restana from 'restana';
import files from 'serve-static'; import files from 'serve-static';
import path from 'path'; import path from 'path';
import { getDirName } from '../utils.js'; import { getDirName } from '../utils.js';
import {demoRouter} from './routes/demoRouter.js';
const service = restana(); const service = restana();
const staticService = files(path.join(getDirName(), '../ui/public')); const staticService = files(path.join(getDirName(), '../ui/public'));
const PORT = config.port || 9998; const PORT = config.port || 9998;
@@ -30,6 +31,9 @@ service.use('/api/jobs/insights', analyticsRouter);
service.use('/api/admin/users', userRouter); service.use('/api/admin/users', userRouter);
service.use('/api/jobs', jobRouter); service.use('/api/jobs', jobRouter);
service.use('/api/login', loginRouter); service.use('/api/login', loginRouter);
//this route is unsecured intentionally as it is being queried from the login page
service.use('/api/demo', demoRouter);
/* eslint-disable no-console */ /* eslint-disable no-console */
service.start(PORT).then(() => { service.start(PORT).then(() => {
console.info(`Started API service on port ${PORT}`); console.info(`Started API service on port ${PORT}`);

View File

@@ -0,0 +1,11 @@
import restana from 'restana';
import {config} from '../../utils.js';
const service = restana();
const demoRouter = service.newRouter();
demoRouter.get('/', async (req, res) => {
res.body = Object.assign({}, {demoMode: config.demoMode});
res.send();
});
export { demoRouter };

View File

@@ -1,6 +1,7 @@
import restana from 'restana'; import restana from 'restana';
import {config, getDirName, readConfigFromStorage, refreshConfig} from '../../utils.js'; import {config, getDirName, readConfigFromStorage, refreshConfig} from '../../utils.js';
import fs from 'fs'; import fs from 'fs';
import {handleDemoUser} from '../../services/storage/userStorage.js';
const service = restana(); const service = restana();
const generalSettingsRouter = service.newRouter(); const generalSettingsRouter = service.newRouter();
generalSettingsRouter.get('/', async (req, res) => { generalSettingsRouter.get('/', async (req, res) => {
@@ -10,9 +11,14 @@ generalSettingsRouter.get('/', async (req, res) => {
generalSettingsRouter.post('/', async (req, res) => { generalSettingsRouter.post('/', async (req, res) => {
const settings = req.body; const settings = req.body;
try { try {
if(config.demoMode){
res.send(new Error('In demo mode, it is not allowed to change these settings.'));
return;
}
const currentConfig = await readConfigFromStorage(); const currentConfig = await readConfigFromStorage();
fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify({...currentConfig, ...settings})); fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify({...currentConfig, ...settings}));
await refreshConfig(); await refreshConfig();
handleDemoUser();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.send(new Error('Error while trying to write settings.')); res.send(new Error('Error while trying to write settings.'));

View File

@@ -6,6 +6,7 @@ import * as immoscoutProvider from '../../provider/immoscout.js';
import { config } from '../../utils.js'; import { config } from '../../utils.js';
import { isAdmin } from '../security.js'; import { isAdmin } from '../security.js';
import {isScrapingAntApiKeySet} from '../../services/scrapingAnt.js'; import {isScrapingAntApiKeySet} from '../../services/scrapingAnt.js';
import {trackDemoJobCreated} from '../../services/tracking/Tracker.js';
const service = restana(); const service = restana();
const jobRouter = service.newRouter(); const jobRouter = service.newRouter();
function doesJobBelongsToUser(job, req) { function doesJobBelongsToUser(job, req) {
@@ -68,6 +69,11 @@ jobRouter.post('/', async (req, res) => {
res.send(new Error(error)); res.send(new Error(error));
console.error(error); console.error(error);
} }
trackDemoJobCreated({
name,
provider,
adapter: notificationAdapter
});
res.send(); res.send();
}); });
jobRouter.delete('', async (req, res) => { jobRouter.delete('', async (req, res) => {

View File

@@ -1,6 +1,8 @@
import restana from 'restana'; import restana from 'restana';
import * as userStorage from '../../services/storage/userStorage.js'; import * as userStorage from '../../services/storage/userStorage.js';
import * as hasher from '../../services/security/hash.js'; import * as hasher from '../../services/security/hash.js';
import {config} from '../../utils.js';
import {trackDemoAccessed} from '../../services/tracking/Tracker.js';
const service = restana(); const service = restana();
const loginRouter = service.newRouter(); const loginRouter = service.newRouter();
loginRouter.get('/user', async (req, res) => { loginRouter.get('/user', async (req, res) => {
@@ -24,6 +26,11 @@ loginRouter.post('/', async (req, res) => {
return; return;
} }
if (user.password === hasher.hash(password)) { if (user.password === hasher.hash(password)) {
if(config.demoMode){
trackDemoAccessed();
}
req.session.currentUser = user.id; req.session.currentUser = user.id;
userStorage.setLastLoginToNow({ userId: user.id }); userStorage.setLastLoginToNow({ userId: user.id });
res.send(200); res.send(200);

View File

@@ -1,6 +1,7 @@
import restana from 'restana'; import restana from 'restana';
import * as userStorage from '../../services/storage/userStorage.js'; import * as userStorage from '../../services/storage/userStorage.js';
import * as jobStorage from '../../services/storage/jobStorage.js'; import * as jobStorage from '../../services/storage/jobStorage.js';
import {config} from '../../utils.js';
const service = restana(); const service = restana();
const userRouter = service.newRouter(); const userRouter = service.newRouter();
function checkIfAnyAdminAfterRemovingUser(userIdToBeRemoved, allUser) { function checkIfAnyAdminAfterRemovingUser(userIdToBeRemoved, allUser) {
@@ -20,6 +21,11 @@ userRouter.get('/:userId', async (req, res) => {
res.send(); res.send();
}); });
userRouter.delete('/', async (req, res) => { userRouter.delete('/', async (req, res) => {
if(config.demoMode){
res.send(new Error('In demo mode, it is not allowed to remove user.'));
return;
}
const { userId } = req.body; const { userId } = req.body;
const allUser = userStorage.getUsers(false); const allUser = userStorage.getUsers(false);
if (!checkIfAnyAdminAfterRemovingUser(userId, allUser)) { if (!checkIfAnyAdminAfterRemovingUser(userId, allUser)) {
@@ -36,6 +42,12 @@ userRouter.delete('/', async (req, res) => {
res.send(); res.send();
}); });
userRouter.post('/', async (req, res) => { userRouter.post('/', async (req, res) => {
if(config.demoMode){
res.send(new Error('In demo mode, it is not allowed to change or add user.'));
return;
}
const { username, password, password2, isAdmin, userId } = req.body; const { username, password, password2, isAdmin, userId } = req.body;
if (password !== password2) { if (password !== password2) {
res.send(new Error('Passwords does not match')); res.send(new Error('Passwords does not match'));

View File

@@ -0,0 +1,29 @@
import { setInterval } from 'node:timers';
import {removeJobsByUserName} from './storage/jobStorage.js';
import {config} from '../utils.js';
/**
* if we are running in demo environment, we have to cleanup the db files (specifically the jobs table)
*/
export function cleanupDemoAtMidnight() {
const now = new Date();
const millisUntilMidnightUTC = (24 - now.getUTCHours()) * 60 * 60 * 1000
- now.getUTCMinutes() * 60 * 1000
- now.getUTCSeconds() * 1000
- now.getUTCMilliseconds();
setTimeout(() => {
cleanup();
setInterval(() => {
cleanup();
}, 24 * 60 * 60 * 1000);
}, millisUntilMidnightUTC);
}
function cleanup(){
if(config.demoMode){
removeJobsByUserName('demo');
}
}

View File

@@ -77,6 +77,17 @@ export const removeJobsByUserId = (userId) => {
.value(); .value();
db.write(); db.write();
}; };
export const removeJobsByUserName = (userName) => {
db.chain
.get('jobs')
.filter((job) => job.username === userName)
.forEach((job) => listingStorage.removeListings(job.id));
db.chain
.get('jobs')
.remove((job) => job.username === userName)
.value();
db.write();
};
export const getJobs = () => { export const getJobs = () => {
return db.chain return db.chain
.get('jobs') .get('jobs')

View File

@@ -1,5 +1,5 @@
import { JSONFileSync } from 'lowdb/node'; import { JSONFileSync } from 'lowdb/node';
import { getDirName } from '../../utils.js'; import {config, getDirName} from '../../utils.js';
import * as hasher from '../security/hash.js'; import * as hasher from '../security/hash.js';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import * as jobStorage from './jobStorage.js'; import * as jobStorage from './jobStorage.js';
@@ -16,6 +16,13 @@ const defaultData = {
password: hasher.hash('admin'), password: hasher.hash('admin'),
isAdmin: true, isAdmin: true,
}, },
{
id: nanoid(),
lastLogin: Date.now(),
username: 'demo',
password: hasher.hash('demo'),
isAdmin: true,
},
], ],
}; };
@@ -84,3 +91,29 @@ export const removeUser = (userId) => {
.value(); .value();
db.write(); db.write();
}; };
export const handleDemoUser = () => {
if(!config.demoMode){
const user = db.chain.get('user').value();
db.chain.get('user').value();
db.chain.set('user', user.filter((u) => u.username !== 'demo')).value();
db.write();
}else {
const demoUser = db.chain
.get('user')
.filter((u) => u.username === 'demo')
.value();
if (demoUser == null || demoUser.length === 0) {
db.chain.get('user')
.value()
.push({
id: nanoid(),
username: 'demo',
password: hasher.hash('demo'),
isAdmin: true,
});
db.write();
}
}
};

View File

@@ -1,42 +1,72 @@
import Mixpanel from 'mixpanel'; import Mixpanel from 'mixpanel';
import {getJobs} from '../storage/jobStorage.js'; import { getJobs } from '../storage/jobStorage.js';
import { getUniqueId } from './uniqueId.js';
import { config, inDevMode } from '../../utils.js';
import {config} from '../../utils.js'; const mixpanelTracker = Mixpanel.init('718670ef1c58c0208256c1e408a3d75e');
const distinct_id = getUniqueId() || 'N/A';
export const track = function () { export const track = function () {
//only send tracking information if the user allowed to do so. //only send tracking information if the user allowed to do so.
if (config.analyticsEnabled) { if (config.analyticsEnabled && !inDevMode()) {
const activeProvider = new Set();
const activeAdapter = new Set();
const mixpanelTracker = Mixpanel.init('718670ef1c58c0208256c1e408a3d75e'); const jobs = getJobs();
const activeProvider = new Set(); if (jobs != null && jobs.length > 0) {
const activeAdapter = new Set(); jobs.forEach((job) => {
const platform = process.platform; job.provider.forEach((provider) => {
const arch = process.arch; activeProvider.add(provider.id);
const language = process.env.LANG || 'en'; });
const nodeVersion = process.version || 'N/A'; job.notificationAdapter.forEach((adapter) => {
activeAdapter.add(adapter.id);
});
});
const jobs = getJobs(); mixpanelTracker.track(
'fredy_tracking',
if (jobs != null && jobs.length > 0) { enrichTrackingObject({
jobs.forEach(job => { adapter: Array.from(activeAdapter),
job.provider.forEach(provider => { provider: Array.from(activeProvider),
activeProvider.add(provider.id); }),
}); );
job.notificationAdapter.forEach(adapter => {
activeAdapter.add(adapter.id);
});
});
mixpanelTracker.track('fredy_tracking', {
adapter: Array.from(activeAdapter),
provider: Array.from(activeProvider),
isDemo: config.demoMode,
platform,
arch,
nodeVersion,
language
});
}
} }
}; }
};
/**
* Note, this will only be used when Fredy runs in demo mode
*/
export function trackDemoJobCreated(jobData) {
if (config.analyticsEnabled && !inDevMode() && config.demoMode) {
mixpanelTracker.track('demoJobCreated', enrichTrackingObject(jobData));
}
}
/**
* Note, this will only be used when Fredy runs in demo mode
*/
export function trackDemoAccessed() {
if (config.analyticsEnabled && !inDevMode() && config.demoMode) {
mixpanelTracker.track('demoAccessed', enrichTrackingObject({}));
}
}
function enrichTrackingObject(trackingObject) {
const platform = process.platform;
const arch = process.arch;
const language = process.env.LANG || 'en';
const nodeVersion = process.version || 'N/A';
return {
...trackingObject,
isDemo: config.demoMode,
platform,
arch,
nodeVersion,
language,
distinct_id,
};
}

View File

@@ -0,0 +1,19 @@
import { hostname, arch, cpus, platform } from 'os';
import { createHash } from 'crypto';
/**
* Don't worry, we are not evil ;) We however need a unique id per running instance
* @returns {string}
*/
export const getUniqueId = () => {
const systemInfo = {
hostname: hostname(),
architecture: arch(),
cpuCount: cpus().length,
platform: platform(),
};
const baseData = JSON.stringify(systemInfo);
return createHash('sha256').update(baseData).digest('hex');
};

View File

@@ -4,6 +4,10 @@ import {readFile} from 'fs/promises';
import {createHash} from 'crypto'; import {createHash} from 'crypto';
import {DEFAULT_CONFIG} from './defaultConfig.js'; import {DEFAULT_CONFIG} from './defaultConfig.js';
function inDevMode(){
return process.env.NODE_ENV == null || process.env.NODE_ENV !== 'production';
}
function isOneOf(word, arr) { function isOneOf(word, arr) {
if (arr == null || arr.length === 0) { if (arr == null || arr.length === 0) {
return false; return false;
@@ -72,6 +76,7 @@ export async function refreshConfig(){
await refreshConfig(); await refreshConfig();
export {isOneOf}; export {isOneOf};
export {inDevMode};
export {nullOrEmpty}; export {nullOrEmpty};
export {duringWorkingHoursOrNotSet}; export {duringWorkingHoursOrNotSet};
export {getDirName}; export {getDirName};

View File

@@ -1,9 +1,9 @@
{ {
"name": "fredy", "name": "fredy",
"version": "10.3.0", "version": "10.4.1",
"description": "[F]ind [R]eal [E]states [d]amn eas[y].", "description": "[F]ind [R]eal [E]states [d]amn eas[y].",
"scripts": { "scripts": {
"start": "node index.js", "start": "node prod.js",
"dev": "yarn && rm -rf ./ui/public/* && vite", "dev": "yarn && rm -rf ./ui/public/* && vite",
"ui": "rm -rf ./ui/public/* && vite", "ui": "rm -rf ./ui/public/* && vite",
"prod": "yarn && vite build --emptyOutDir", "prod": "yarn && vite build --emptyOutDir",
@@ -50,22 +50,22 @@
"Firefox ESR" "Firefox ESR"
], ],
"dependencies": { "dependencies": {
"@douyinfe/semi-ui": "2.69.2", "@douyinfe/semi-ui": "2.70.1",
"@rematch/core": "2.2.0", "@rematch/core": "2.2.0",
"@rematch/loading": "2.1.2", "@rematch/loading": "2.1.2",
"@sendgrid/mail": "8.1.4", "@sendgrid/mail": "8.1.4",
"@vitejs/plugin-react": "4.3.3", "@vitejs/plugin-react": "4.3.4",
"better-sqlite3": "^11.5.0", "better-sqlite3": "^11.6.0",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"cookie-session": "2.1.0", "cookie-session": "2.1.0",
"handlebars": "4.7.8", "handlebars": "4.7.8",
"highcharts": "11.4.8", "highcharts": "12.0.1",
"highcharts-react-official": "3.2.1", "highcharts-react-official": "3.2.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"lowdb": "6.0.1", "lowdb": "6.0.1",
"markdown": "^0.5.0", "markdown": "^0.5.0",
"mixpanel": "^0.18.0", "mixpanel": "^0.18.0",
"nanoid": "5.0.8", "nanoid": "5.0.9",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"node-mailjet": "6.0.6", "node-mailjet": "6.0.6",
"query-string": "9.1.1", "query-string": "9.1.1",
@@ -95,7 +95,7 @@
"esmock": "2.6.9", "esmock": "2.6.9",
"history": "5.3.0", "history": "5.3.0",
"husky": "9.1.7", "husky": "9.1.7",
"less": "4.2.0", "less": "4.2.1",
"lint-staged": "15.2.10", "lint-staged": "15.2.10",
"mocha": "10.8.2", "mocha": "10.8.2",
"prettier": "3.3.3", "prettier": "3.3.3",

2
prod.js Normal file
View File

@@ -0,0 +1,2 @@
process.env.NODE_ENV = 'production';
import('./index.js');

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, {useEffect} from 'react';
import InsufficientPermission from './components/permission/InsufficientPermission'; import InsufficientPermission from './components/permission/InsufficientPermission';
import PermissionAwareRoute from './components/permission/PermissionAwareRoute'; import PermissionAwareRoute from './components/permission/PermissionAwareRoute';
@@ -6,94 +6,106 @@ import GeneralSettings from './views/generalSettings/GeneralSettings';
import JobMutation from './views/jobs/mutation/JobMutation'; import JobMutation from './views/jobs/mutation/JobMutation';
import UserMutator from './views/user/mutation/UserMutator'; import UserMutator from './views/user/mutation/UserMutator';
import JobInsight from './views/jobs/insights/JobInsight.jsx'; import JobInsight from './views/jobs/insights/JobInsight.jsx';
import { useDispatch, useSelector } from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import { Switch, Redirect } from 'react-router-dom'; import {Switch, Redirect} from 'react-router-dom';
import Logout from './components/logout/Logout'; import Logout from './components/logout/Logout';
import Logo from './components/logo/Logo'; import Logo from './components/logo/Logo';
import Menu from './components/menu/Menu'; import Menu from './components/menu/Menu';
import Login from './views/login/Login'; import Login from './views/login/Login';
import Users from './views/user/Users'; import Users from './views/user/Users';
import Jobs from './views/jobs/Jobs'; import Jobs from './views/jobs/Jobs';
import { Route } from 'react-router'; import {Route} from 'react-router';
import './App.less'; import './App.less';
import TrackingModal from './components/tracking/TrackingModal.jsx'; import TrackingModal from './components/tracking/TrackingModal.jsx';
import {Banner} from '@douyinfe/semi-ui';
export default function FredyApp() { export default function FredyApp() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const currentUser = useSelector((state) => state.user.currentUser); const currentUser = useSelector((state) => state.user.currentUser);
const settings = useSelector((state) => state.generalSettings.settings); const settings = useSelector((state) => state.generalSettings.settings);
useEffect(() => { useEffect(() => {
async function init() { async function init() {
await dispatch.user.getCurrentUser(); await dispatch.user.getCurrentUser();
if (!needsLogin()) { if (!needsLogin()) {
await dispatch.provider.getProvider(); await dispatch.provider.getProvider();
await dispatch.jobs.getJobs(); await dispatch.jobs.getJobs();
await dispatch.jobs.getProcessingTimes(); await dispatch.jobs.getProcessingTimes();
await dispatch.notificationAdapter.getAdapter(); await dispatch.notificationAdapter.getAdapter();
await dispatch.generalSettings.getGeneralSettings(); await dispatch.generalSettings.getGeneralSettings();
} }
setLoading(false); setLoading(false);
} }
init(); init();
}, [currentUser?.userId]); }, [currentUser?.userId]);
const needsLogin = () => { const needsLogin = () => {
return currentUser == null || Object.keys(currentUser).length === 0; 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.analyticsEnabled === null && <TrackingModal/>}
<Switch> <Switch>
<Route name="Insufficient Permission" path={'/403'} component={InsufficientPermission} /> <Route name="Login" path={'/login'} component={Login}/>
<Route name="Create new Job" path={'/jobs/new'} component={JobMutation} /> <Redirect from="*" to={'/login'}/>
<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> </Switch>
</div> );
</div>
); 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>
);
} }
FredyApp.displayName = 'FredyApp'; FredyApp.displayName = 'FredyApp';

View File

@@ -0,0 +1,24 @@
import { xhrGet } from '../../xhr';
export const demoMode = {
state: {
demoMode: false,
},
reducers: {
setDemoMode: (state, payload) => {
return {
...state,
demoMode: payload.demoMode,
};
},
},
effects: {
async getDemoMode() {
try {
const response = await xhrGet('/api/demo');
this.setDemoMode(response.json);
} catch (Exception) {
console.error('Error while trying to get resource for api/demo. Error:', Exception);
}
},
},
};

View File

@@ -5,6 +5,7 @@ import { provider } from './models/provider';
import { createLogger } from 'redux-logger'; import { createLogger } from 'redux-logger';
import { jobs } from './models/jobs'; import { jobs } from './models/jobs';
import { user } from './models/user'; import { user } from './models/user';
import { demoMode } from './models/demoMode.js';
import { init } from '@rematch/core'; import { init } from '@rematch/core';
const middleware = []; const middleware = [];
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
@@ -16,6 +17,7 @@ const store = init({
models: { models: {
notificationAdapter, notificationAdapter,
generalSettings, generalSettings,
demoMode,
provider, provider,
jobs, jobs,
user, user,

View File

@@ -59,7 +59,7 @@ const GeneralSettings = function GeneralSettings() {
setWorkingHourFrom(settings?.workingHours?.from); setWorkingHourFrom(settings?.workingHours?.from);
setWorkingHourTo(settings?.workingHours?.to); setWorkingHourTo(settings?.workingHours?.to);
setScrapingAntProxy(settings?.scrapingAnt?.proxy || 'datacenter'); setScrapingAntProxy(settings?.scrapingAnt?.proxy || 'datacenter');
setAnalyticsEnabled(settings?.analytics || false); setAnalyticsEnabled(settings?.analyticsEnabled || false);
setDemoMode(settings?.demoMode || false); setDemoMode(settings?.demoMode || false);
} }
@@ -109,10 +109,17 @@ const GeneralSettings = function GeneralSettings() {
}); });
} catch (exception) { } catch (exception) {
console.error(exception); console.error(exception);
throwMessage('Error while trying to store settings.', 'error'); if(exception?.json?.message != null){
throwMessage(exception.json.message, 'error');
}else {
throwMessage('Error while trying to store settings.', 'error');
}
return; return;
} }
throwMessage('Settings stored successfully.', 'success'); throwMessage('Settings stored successfully. We will reload your browser in 3 seconds.', 'success');
setTimeout(()=>{
location.reload();
}, 3000);
}; };
return ( return (
@@ -120,14 +127,6 @@ const GeneralSettings = function GeneralSettings() {
{!loading && ( {!loading && (
<React.Fragment> <React.Fragment>
<Headline text="General Settings"/> <Headline text="General Settings"/>
<Banner
fullMode={false}
type="info"
closeIcon={null}
title={<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>Info</div>}
style={{marginBottom: '1rem'}}
description="If you change any settings, you must restart Fredy afterwards."
/>
<div> <div>
<SegmentPart <SegmentPart
name="Interval" name="Interval"

View File

@@ -1,88 +1,102 @@
import React from 'react'; import React, {useEffect} from 'react';
import cityBackground from '../../assets/city_background.jpg'; import cityBackground from '../../assets/city_background.jpg';
import Logo from '../../components/logo/Logo'; import Logo from '../../components/logo/Logo';
import { xhrPost } from '../../services/xhr'; import {xhrPost} from '../../services/xhr';
import { useHistory } from 'react-router'; import {useHistory} from 'react-router';
import { useDispatch } from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import { Input, Button, Banner } from '@douyinfe/semi-ui'; import {Input, Button, Banner} from '@douyinfe/semi-ui';
import './login.less'; import './login.less';
import { IconUser, IconLock } from '@douyinfe/semi-icons'; import {IconUser, IconLock} from '@douyinfe/semi-icons';
export default function Login() { export default function Login() {
const dispatch = useDispatch(); 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 [username, setUserName] = React.useState(''); useEffect(() => {
const [password, setPassword] = React.useState(''); async function init() {
const [error, setError] = React.useState(null); await dispatch.demoMode.getDemoMode();
}
const history = useHistory(); init();
}, []);
const tryLogin = async () => { const tryLogin = async () => {
if (username.length === 0 || password.length === 0) { if (username.length === 0 || password.length === 0) {
setError('Username and password are mandatory.'); setError('Username and password are mandatory.');
return; return;
} }
try { try {
await xhrPost('/api/login', { await xhrPost('/api/login', {
username, username,
password, password,
}); });
setError(null); setError(null);
} catch (Exception) { } catch (Exception) {
setError('Login not successful...'); setError('Login not successful...');
return; return;
} }
await dispatch.user.getCurrentUser(); await dispatch.user.getCurrentUser();
history.push('/jobs'); history.push('/jobs');
}; };
return ( return (
<div className="login"> <div className="login">
<div className="login__bgImage" style={{ background: `url("${cityBackground}")` }} /> <div className="login__bgImage" style={{background: `url("${cityBackground}")`}}/>
<Logo /> <Logo/>
<form> <form>
<div className="login__loginWrapper"> <div className="login__loginWrapper">
{error && <Banner type="danger" closeIcon={null} description={error} />} {error && <Banner type="danger" closeIcon={null} description={error}/>}
<Input <Input
size="large" size="large"
prefix={<IconUser />} prefix={<IconUser/>}
placeholder="Username" placeholder="Username"
value={username} value={username}
showClear showClear
style={{ marginTop: error ? '1rem' : '4rem' }} style={{marginTop: error ? '1rem' : '4rem'}}
autofocus autoFocus
onChange={(value) => setUserName(value)} onChange={(value) => setUserName(value)}
onKeyPress={async (e) => { onKeyPress={async (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
await tryLogin(); await tryLogin();
} }
}} }}
/> />
<Input <Input
size="large" size="large"
mode="password" mode="password"
prefix={<IconLock />} prefix={<IconLock/>}
value={password} value={password}
placeholder="Password" placeholder="Password"
style={{ marginTop: '2rem' }} style={{marginTop: '2rem'}}
onChange={(value) => setPassword(value)} onChange={(value) => setPassword(value)}
onKeyPress={async (e) => { onKeyPress={async (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
await tryLogin(); await tryLogin();
} }
}} }}
/> />
<Button type="primary" onClick={tryLogin} theme="solid" style={{ marginTop: '3rem' }}> <Button type="primary" onClick={tryLogin} theme="solid" style={{marginTop: '3rem'}}>
Login Login
</Button> </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> </div>
</form> );
</div>
);
} }
Login.displayName = 'Login'; Login.displayName = 'Login';

134
yarn.lock
View File

@@ -43,7 +43,7 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e"
integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg== integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==
"@babel/core@7.26.0", "@babel/core@^7.25.2": "@babel/core@7.26.0", "@babel/core@^7.26.0":
version "7.26.0" version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40"
integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==
@@ -695,14 +695,14 @@
dependencies: dependencies:
"@babel/plugin-transform-react-jsx" "^7.25.9" "@babel/plugin-transform-react-jsx" "^7.25.9"
"@babel/plugin-transform-react-jsx-self@^7.24.7": "@babel/plugin-transform-react-jsx-self@^7.25.9":
version "7.25.9" version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz#c0b6cae9c1b73967f7f9eb2fca9536ba2fad2858" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz#c0b6cae9c1b73967f7f9eb2fca9536ba2fad2858"
integrity sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg== integrity sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.25.9" "@babel/helper-plugin-utils" "^7.25.9"
"@babel/plugin-transform-react-jsx-source@^7.24.7": "@babel/plugin-transform-react-jsx-source@^7.25.9":
version "7.25.9" version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz#4c6b8daa520b5f155b5fb55547d7c9fa91417503" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz#4c6b8daa520b5f155b5fb55547d7c9fa91417503"
integrity sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg== integrity sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==
@@ -991,33 +991,33 @@
dependencies: dependencies:
tslib "^2.0.0" tslib "^2.0.0"
"@douyinfe/semi-animation-react@2.69.2": "@douyinfe/semi-animation-react@2.70.1":
version "2.69.2" version "2.70.1"
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.69.2.tgz#b47565c64dae7f4e1a7c5a9a21a244d59986b3fd" resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.70.1.tgz#1fe840336a3a49b3d2ed0bac3479135fe6af1a31"
integrity sha512-N6bdju90nnQdNHmnp5C8n8oqSDqqzgO6rzCPwwb6Ef4+aC/csdU1/Dsdp6JA6QKQ768oHGPT5YJs3QiKSGZZcw== integrity sha512-xiryCQGhjGUZ4/j5WIfjtn9s8fZsTlSxmebbmZK576qm05020YCc92r52g8z1SgXDBluOqs+OLrrWJw9pq6IHg==
dependencies: dependencies:
"@douyinfe/semi-animation" "2.69.2" "@douyinfe/semi-animation" "2.70.1"
"@douyinfe/semi-animation-styled" "2.69.2" "@douyinfe/semi-animation-styled" "2.70.1"
classnames "^2.2.6" classnames "^2.2.6"
"@douyinfe/semi-animation-styled@2.69.2": "@douyinfe/semi-animation-styled@2.70.1":
version "2.69.2" version "2.70.1"
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.69.2.tgz#18c16a959c92e908aa4fad521fe7e0fe83296034" resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.70.1.tgz#4f094a90bad134b97c4ebdb7b611a6a7f956f6d7"
integrity sha512-HHHR2qS7BRCtP78qp9N/OL9RWPvoxxRg6uC6kUm8l4t5FCcr0QrdhkzYIpohAVa90BedpTPwhRHhh3aiXfnx9A== integrity sha512-6ESi+RruTF6U+PzBKT+5DcK1MCwryu1B9nvZiwXflXRRRzi37OzvDy68g3uo/btcpeUvWN8Oi0G+Lg2t8z3xQQ==
"@douyinfe/semi-animation@2.69.2": "@douyinfe/semi-animation@2.70.1":
version "2.69.2" version "2.70.1"
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.69.2.tgz#4023340747eb202f5e3b2d48dfd4efea94d815cb" resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.70.1.tgz#f200e65d9f234ddc2124bb9b09d99a602dc4280f"
integrity sha512-elut0fb5eKr5pnrZKgaOS97nw+KxkoL4N+tho4u099a3K5GFwzvyzVPOK0ALReCWnO0tSxUbwPpUQxMomG+vKA== integrity sha512-CZBuMsVETxgau3j3jCr6pur3vZk3Sk1BZx4CBaD4EzWx4mXNY3aNRwaNwcuR3TWl0zxLX2HTpLgMPe4X+aW+/g==
dependencies: dependencies:
bezier-easing "^2.1.0" bezier-easing "^2.1.0"
"@douyinfe/semi-foundation@2.69.2": "@douyinfe/semi-foundation@2.70.1":
version "2.69.2" version "2.70.1"
resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.69.2.tgz#2a782760511e509410df87e473e956465e8b1a6f" resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.70.1.tgz#de5b8ec06ddc94c8e1ba747a3ca14fe9c9ff8e41"
integrity sha512-qiN1uBxEs+ofIAGOw6oF7AgTXDlfgmbl9xYTDsS5D3tSH0p0hjAsqW2d59/lScr3P7dYzxKOXZ6aJrC5TXW3Wg== integrity sha512-+A6LU5Jqqqgn1BG4n5r8TDKbfT8eSW5XKhGw9eekfnbQkfu5EEgsyZQGo2KKwh7TKUlIc6ntrAH4skviPMqJ6Q==
dependencies: dependencies:
"@douyinfe/semi-animation" "2.69.2" "@douyinfe/semi-animation" "2.70.1"
"@mdx-js/mdx" "^3.0.1" "@mdx-js/mdx" "^3.0.1"
async-validator "^3.5.0" async-validator "^3.5.0"
classnames "^2.2.6" classnames "^2.2.6"
@@ -1031,37 +1031,37 @@
remark-gfm "^4.0.0" remark-gfm "^4.0.0"
scroll-into-view-if-needed "^2.2.24" scroll-into-view-if-needed "^2.2.24"
"@douyinfe/semi-icons@2.69.2": "@douyinfe/semi-icons@2.70.1":
version "2.69.2" version "2.70.1"
resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.69.2.tgz#92ade6402237c1a98d4f39a6d447a874848ea066" resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.70.1.tgz#382ce7c498deeeb08ed16088cf21b8a319bda20b"
integrity sha512-0Wzb4bd5DYZjlcR9JS2Cv5D7LeSApy0TD4BMp8rHRStp9iNIGnB/Fob7OFAz9ZuceJ8nb+IN4uxQyf0GuNh/tA== integrity sha512-5So+o2nHp468lyRihwzAbRvo0/PQi0rZdKdepyD8tQEEl6ou00bgie/p9/ZShdrWHrG2a3oyFX77pTsGTbEnNQ==
dependencies: dependencies:
classnames "^2.2.6" classnames "^2.2.6"
"@douyinfe/semi-illustrations@2.69.2": "@douyinfe/semi-illustrations@2.70.1":
version "2.69.2" version "2.70.1"
resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.69.2.tgz#aac0c5c65c1363c86ab6dcfd702a0d4d9a3a1c23" resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.70.1.tgz#2c6d870960fa609936c4d560434279b2bb439209"
integrity sha512-rdRB6ZZ2zo2c/e0Hkffq84i0w90BMmhBGskvei8AWDlfiuTJhXL6qZ3ixZiE+fjPQO12mk/QNG+LNv8Tr5yFfQ== integrity sha512-kk87Uf4kPUWe816ywK8OwFafXqL4HWJQgFYE/Tb+LAw4uuGpDQKE0aELlSzR2/QpfUIAsPExqOKNeWvDVSHDZg==
"@douyinfe/semi-theme-default@2.69.2": "@douyinfe/semi-theme-default@2.70.1":
version "2.69.2" version "2.70.1"
resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.69.2.tgz#6256c55f07e34b8f134e5a7a2f9fe85970387bf3" resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.70.1.tgz#fcb844849ae70b6a16155ea0a9bbf0f0f63aed21"
integrity sha512-pyol1EFUwuErp6Tlw+VLs4nVjnj1PSEC5P3YB0MpIcDWZsH/ixRsgMAGuvWI/+owwZ6KnzRDA0t+XcGm+e17qA== integrity sha512-urIcebhGXVeVslf26ujSiQ4YQcqUxyvWimP9AM2IXkt6MtsYlVDpdyiIoOyO4IMxHxfMhFAphh2UXIbdlkTPOA==
"@douyinfe/semi-ui@2.69.2": "@douyinfe/semi-ui@2.70.1":
version "2.69.2" version "2.70.1"
resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.69.2.tgz#9f74898cc865fb01c622aa58f6205cbd11f3aa47" resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.70.1.tgz#6a863663add0d8b31f048e5a63f99820bc087f72"
integrity sha512-oDI3jLlwugpF8vNx6R+ivwTh1hu7Sr8Yrb3+8nsxNlc9+C+RHA01uu4xGmORwHQdYe2+YRn+kFP8+tu2Ch90Nw== integrity sha512-irXuCCDr6oUzzv0usqWqSo11LeqNEi+yPaKTAujv8OnugfDG4e084cCXTa5/E6xSFS3UXJEWD7hUe7NB0+FNHA==
dependencies: dependencies:
"@dnd-kit/core" "^6.0.8" "@dnd-kit/core" "^6.0.8"
"@dnd-kit/sortable" "^7.0.2" "@dnd-kit/sortable" "^7.0.2"
"@dnd-kit/utilities" "^3.2.1" "@dnd-kit/utilities" "^3.2.1"
"@douyinfe/semi-animation" "2.69.2" "@douyinfe/semi-animation" "2.70.1"
"@douyinfe/semi-animation-react" "2.69.2" "@douyinfe/semi-animation-react" "2.70.1"
"@douyinfe/semi-foundation" "2.69.2" "@douyinfe/semi-foundation" "2.70.1"
"@douyinfe/semi-icons" "2.69.2" "@douyinfe/semi-icons" "2.70.1"
"@douyinfe/semi-illustrations" "2.69.2" "@douyinfe/semi-illustrations" "2.70.1"
"@douyinfe/semi-theme-default" "2.69.2" "@douyinfe/semi-theme-default" "2.70.1"
async-validator "^3.5.0" async-validator "^3.5.0"
classnames "^2.2.6" classnames "^2.2.6"
copy-text-to-clipboard "^2.1.1" copy-text-to-clipboard "^2.1.1"
@@ -1587,14 +1587,14 @@
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
"@vitejs/plugin-react@4.3.3": "@vitejs/plugin-react@4.3.4":
version "4.3.3" version "4.3.4"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz#28301ac6d7aaf20b73a418ee5c65b05519b4836c" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz#c64be10b54c4640135a5b28a2432330e88ad7c20"
integrity sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA== integrity sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==
dependencies: dependencies:
"@babel/core" "^7.25.2" "@babel/core" "^7.26.0"
"@babel/plugin-transform-react-jsx-self" "^7.24.7" "@babel/plugin-transform-react-jsx-self" "^7.25.9"
"@babel/plugin-transform-react-jsx-source" "^7.24.7" "@babel/plugin-transform-react-jsx-source" "^7.25.9"
"@types/babel__core" "^7.20.5" "@types/babel__core" "^7.20.5"
react-refresh "^0.14.2" react-refresh "^0.14.2"
@@ -1907,10 +1907,10 @@ batch@~0.6.0:
resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz"
integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
better-sqlite3@^11.5.0: better-sqlite3@^11.6.0:
version "11.5.0" version "11.6.0"
resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-11.5.0.tgz#58faa51e02845a578dd154f0083487132ead0695" resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-11.6.0.tgz#e50736956e6fe1c30dc94f1bc94a9c15d63b7b6b"
integrity sha512-e/6eggfOutzoK0JWiU36jsisdWoHOfN9iWiW/SieKvb7SAa6aGNmBM/UKyp+/wWSXpLlWNN8tCPwoDNPhzUvuQ== integrity sha512-2J6k/eVxcFYY2SsTxsXrj6XylzHWPxveCn4fKPKZFv/Vqn/Cd7lOuX4d7rGQXT5zL+97MkNL3nSbCrIoe3LkgA==
dependencies: dependencies:
bindings "^1.5.0" bindings "^1.5.0"
prebuild-install "^7.1.1" prebuild-install "^7.1.1"
@@ -3923,10 +3923,10 @@ highcharts-react-official@3.2.1:
resolved "https://registry.npmjs.org/highcharts-react-official/-/highcharts-react-official-3.2.1.tgz" resolved "https://registry.npmjs.org/highcharts-react-official/-/highcharts-react-official-3.2.1.tgz"
integrity sha512-hyQTX7ezCxl7JqumaWiGsroGWalzh24GedQIgO3vJbkGOZ6ySRAltIYjfxhrq4HszJOySZegotEF7v+haQ75UA== integrity sha512-hyQTX7ezCxl7JqumaWiGsroGWalzh24GedQIgO3vJbkGOZ6ySRAltIYjfxhrq4HszJOySZegotEF7v+haQ75UA==
highcharts@11.4.8: highcharts@12.0.1:
version "11.4.8" version "12.0.1"
resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-11.4.8.tgz#252e71b81c24ec9f99e756b76dbebd7546e18dda" resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-12.0.1.tgz#a8c45938a510fc23ca5380dbfec5f82cd0eee5c9"
integrity sha512-5Tke9LuzZszC4osaFisxLIcw7xgNGz4Sy3Jc9pRMV+ydm6sYqsPYdU8ELOgpzGNrbrRNDRBtveoR5xS3SzneEA== integrity sha512-86pku0cZnHfEu6uqbbEpU50XKHcAFFXeD4pYvxlVBRDLRDlxpT0WuClgJBuBJZof1ZjFUh1D7IxUxFgt9Epb7Q==
history@5.3.0: history@5.3.0:
version "5.3.0" version "5.3.0"
@@ -4586,10 +4586,10 @@ koa-is-json@^1.0.0:
resolved "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz" resolved "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz"
integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ= integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=
less@4.2.0: less@4.2.1:
version "4.2.0" version "4.2.1"
resolved "https://registry.npmjs.org/less/-/less-4.2.0.tgz" resolved "https://registry.yarnpkg.com/less/-/less-4.2.1.tgz#fe4c9848525ab44614c0cf2c00abd8d031bb619a"
integrity sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA== integrity sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==
dependencies: dependencies:
copy-anything "^2.0.1" copy-anything "^2.0.1"
parse-node-version "^1.0.1" parse-node-version "^1.0.1"
@@ -5530,10 +5530,10 @@ ms@2.1.3, ms@^2.1.3:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@5.0.8: nanoid@5.0.9:
version "5.0.8" version "5.0.9"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.8.tgz#7610003f6b3b761b5c244bb342c112c5312512bf" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.9.tgz#977dcbaac055430ce7b1e19cf0130cea91a20e50"
integrity sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ== integrity sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==
nanoid@^3.3.7: nanoid@^3.3.7:
version "3.3.7" version "3.3.7"