mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8f911aa00 | ||
|
|
13b8701447 | ||
|
|
e25b956eda | ||
|
|
a2c769f786 | ||
|
|
1825a25eaa | ||
|
|
0f20b85f38 | ||
|
|
d17ef9ef1e | ||
|
|
337ee922a6 |
@@ -106,9 +106,7 @@ exports.config = {
|
||||
```
|
||||
|
||||
#### 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?
|
||||
|
||||
To write tests for provider, you need to use Node 8 as the tests are using `async / await`
|
||||
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?
|
||||
|
||||
#### Codestyle
|
||||
I'm using Eslint to maintain quote style and quality. Do not skip it...
|
||||
|
||||
@@ -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
|
||||
|
||||
## Demo
|
||||
If you want to try out _Fredy_, you can access the demo version [here](https://fredy.orange-coding.net) 🤘
|
||||
|
||||
## Usage
|
||||
|
||||
- Make sure to use Node.js 20 or above
|
||||
|
||||
@@ -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}
|
||||
|
||||
54
index.js
54
index.js
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
@@ -7,6 +7,8 @@ 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';
|
||||
//if db folder does not exist, ensure to create it before loading anything else
|
||||
if (!fs.existsSync('./db')) {
|
||||
fs.mkdirSync('./db');
|
||||
@@ -17,35 +19,43 @@ 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();
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
const fetchedProvider = await Promise.all(
|
||||
provider.filter((provider) => provider.endsWith('.js')).map(async (pro) => import(`${path}/${pro}`))
|
||||
);
|
||||
|
||||
handleDemoUser();
|
||||
|
||||
setInterval(
|
||||
(function exec() {
|
||||
const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now());
|
||||
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
|
||||
|
||||
@@ -12,6 +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';
|
||||
const service = restana();
|
||||
const staticService = files(path.join(getDirName(), '../ui/public'));
|
||||
const PORT = config.port || 9998;
|
||||
@@ -30,6 +31,9 @@ service.use('/api/jobs/insights', analyticsRouter);
|
||||
service.use('/api/admin/users', userRouter);
|
||||
service.use('/api/jobs', jobRouter);
|
||||
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 */
|
||||
service.start(PORT).then(() => {
|
||||
console.info(`Started API service on port ${PORT}`);
|
||||
|
||||
11
lib/api/routes/demoRouter.js
Normal file
11
lib/api/routes/demoRouter.js
Normal 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 };
|
||||
@@ -1,6 +1,7 @@
|
||||
import restana from 'restana';
|
||||
import {config, getDirName, readConfigFromStorage, refreshConfig} from '../../utils.js';
|
||||
import fs from 'fs';
|
||||
import {handleDemoUser} from '../../services/storage/userStorage.js';
|
||||
const service = restana();
|
||||
const generalSettingsRouter = service.newRouter();
|
||||
generalSettingsRouter.get('/', async (req, res) => {
|
||||
@@ -10,9 +11,14 @@ generalSettingsRouter.get('/', async (req, res) => {
|
||||
generalSettingsRouter.post('/', async (req, res) => {
|
||||
const settings = req.body;
|
||||
try {
|
||||
if(config.demoMode){
|
||||
res.send(new Error('In demo mode, it is not allowed to change these settings.'));
|
||||
return;
|
||||
}
|
||||
const currentConfig = await readConfigFromStorage();
|
||||
fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify({...currentConfig, ...settings}));
|
||||
await refreshConfig();
|
||||
handleDemoUser();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send(new Error('Error while trying to write settings.'));
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as immoscoutProvider from '../../provider/immoscout.js';
|
||||
import { config } from '../../utils.js';
|
||||
import { isAdmin } from '../security.js';
|
||||
import {isScrapingAntApiKeySet} from '../../services/scrapingAnt.js';
|
||||
import {trackDemoJobCreated} from '../../services/tracking/Tracker.js';
|
||||
const service = restana();
|
||||
const jobRouter = service.newRouter();
|
||||
function doesJobBelongsToUser(job, req) {
|
||||
@@ -68,6 +69,11 @@ jobRouter.post('/', async (req, res) => {
|
||||
res.send(new Error(error));
|
||||
console.error(error);
|
||||
}
|
||||
trackDemoJobCreated({
|
||||
name,
|
||||
provider,
|
||||
adapter: notificationAdapter
|
||||
});
|
||||
res.send();
|
||||
});
|
||||
jobRouter.delete('', async (req, res) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import restana from 'restana';
|
||||
import * as userStorage from '../../services/storage/userStorage.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 loginRouter = service.newRouter();
|
||||
loginRouter.get('/user', async (req, res) => {
|
||||
@@ -24,6 +26,11 @@ loginRouter.post('/', async (req, res) => {
|
||||
return;
|
||||
}
|
||||
if (user.password === hasher.hash(password)) {
|
||||
|
||||
if(config.demoMode){
|
||||
trackDemoAccessed();
|
||||
}
|
||||
|
||||
req.session.currentUser = user.id;
|
||||
userStorage.setLastLoginToNow({ userId: user.id });
|
||||
res.send(200);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import restana from 'restana';
|
||||
import * as userStorage from '../../services/storage/userStorage.js';
|
||||
import * as jobStorage from '../../services/storage/jobStorage.js';
|
||||
import {config} from '../../utils.js';
|
||||
const service = restana();
|
||||
const userRouter = service.newRouter();
|
||||
function checkIfAnyAdminAfterRemovingUser(userIdToBeRemoved, allUser) {
|
||||
@@ -20,6 +21,11 @@ userRouter.get('/:userId', async (req, res) => {
|
||||
res.send();
|
||||
});
|
||||
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 allUser = userStorage.getUsers(false);
|
||||
if (!checkIfAnyAdminAfterRemovingUser(userId, allUser)) {
|
||||
@@ -36,6 +42,12 @@ userRouter.delete('/', async (req, res) => {
|
||||
res.send();
|
||||
});
|
||||
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;
|
||||
if (password !== password2) {
|
||||
res.send(new Error('Passwords does not match'));
|
||||
|
||||
29
lib/services/demoCleanup.js
Normal file
29
lib/services/demoCleanup.js
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,17 @@ export const removeJobsByUserId = (userId) => {
|
||||
.value();
|
||||
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 = () => {
|
||||
return db.chain
|
||||
.get('jobs')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { JSONFileSync } from 'lowdb/node';
|
||||
import { getDirName } from '../../utils.js';
|
||||
import {config, getDirName} from '../../utils.js';
|
||||
import * as hasher from '../security/hash.js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import * as jobStorage from './jobStorage.js';
|
||||
@@ -16,6 +16,13 @@ const defaultData = {
|
||||
password: hasher.hash('admin'),
|
||||
isAdmin: true,
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
lastLogin: Date.now(),
|
||||
username: 'demo',
|
||||
password: hasher.hash('demo'),
|
||||
isAdmin: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -84,3 +91,29 @@ export const removeUser = (userId) => {
|
||||
.value();
|
||||
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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,42 +1,72 @@
|
||||
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 () {
|
||||
//only send tracking information if the user allowed to do so.
|
||||
if (config.analyticsEnabled) {
|
||||
//only send tracking information if the user allowed to do so.
|
||||
if (config.analyticsEnabled && !inDevMode()) {
|
||||
const activeProvider = new Set();
|
||||
const activeAdapter = new Set();
|
||||
|
||||
const mixpanelTracker = Mixpanel.init('718670ef1c58c0208256c1e408a3d75e');
|
||||
const jobs = getJobs();
|
||||
|
||||
const activeProvider = new Set();
|
||||
const activeAdapter = new Set();
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
const language = process.env.LANG || 'en';
|
||||
const nodeVersion = process.version || 'N/A';
|
||||
if (jobs != null && jobs.length > 0) {
|
||||
jobs.forEach((job) => {
|
||||
job.provider.forEach((provider) => {
|
||||
activeProvider.add(provider.id);
|
||||
});
|
||||
job.notificationAdapter.forEach((adapter) => {
|
||||
activeAdapter.add(adapter.id);
|
||||
});
|
||||
});
|
||||
|
||||
const jobs = getJobs();
|
||||
|
||||
if (jobs != null && jobs.length > 0) {
|
||||
jobs.forEach(job => {
|
||||
job.provider.forEach(provider => {
|
||||
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
|
||||
});
|
||||
}
|
||||
mixpanelTracker.track(
|
||||
'fredy_tracking',
|
||||
enrichTrackingObject({
|
||||
adapter: Array.from(activeAdapter),
|
||||
provider: Array.from(activeProvider),
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
|
||||
19
lib/services/tracking/uniqueId.js
Normal file
19
lib/services/tracking/uniqueId.js
Normal 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');
|
||||
};
|
||||
@@ -4,6 +4,10 @@ import {readFile} from 'fs/promises';
|
||||
import {createHash} from 'crypto';
|
||||
import {DEFAULT_CONFIG} from './defaultConfig.js';
|
||||
|
||||
function inDevMode(){
|
||||
return process.env.NODE_ENV == null || process.env.NODE_ENV !== 'production';
|
||||
}
|
||||
|
||||
function isOneOf(word, arr) {
|
||||
if (arr == null || arr.length === 0) {
|
||||
return false;
|
||||
@@ -72,6 +76,7 @@ export async function refreshConfig(){
|
||||
await refreshConfig();
|
||||
|
||||
export {isOneOf};
|
||||
export {inDevMode};
|
||||
export {nullOrEmpty};
|
||||
export {duringWorkingHoursOrNotSet};
|
||||
export {getDirName};
|
||||
|
||||
16
package.json
16
package.json
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "fredy",
|
||||
"version": "10.3.0",
|
||||
"version": "10.4.1",
|
||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"start": "node prod.js",
|
||||
"dev": "yarn && rm -rf ./ui/public/* && vite",
|
||||
"ui": "rm -rf ./ui/public/* && vite",
|
||||
"prod": "yarn && vite build --emptyOutDir",
|
||||
@@ -50,22 +50,22 @@
|
||||
"Firefox ESR"
|
||||
],
|
||||
"dependencies": {
|
||||
"@douyinfe/semi-ui": "2.69.2",
|
||||
"@douyinfe/semi-ui": "2.70.1",
|
||||
"@rematch/core": "2.2.0",
|
||||
"@rematch/loading": "2.1.2",
|
||||
"@sendgrid/mail": "8.1.4",
|
||||
"@vitejs/plugin-react": "4.3.3",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"better-sqlite3": "^11.6.0",
|
||||
"body-parser": "1.20.3",
|
||||
"cookie-session": "2.1.0",
|
||||
"handlebars": "4.7.8",
|
||||
"highcharts": "11.4.8",
|
||||
"highcharts": "12.0.1",
|
||||
"highcharts-react-official": "3.2.1",
|
||||
"lodash": "4.17.21",
|
||||
"lowdb": "6.0.1",
|
||||
"markdown": "^0.5.0",
|
||||
"mixpanel": "^0.18.0",
|
||||
"nanoid": "5.0.8",
|
||||
"nanoid": "5.0.9",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-mailjet": "6.0.6",
|
||||
"query-string": "9.1.1",
|
||||
@@ -95,7 +95,7 @@
|
||||
"esmock": "2.6.9",
|
||||
"history": "5.3.0",
|
||||
"husky": "9.1.7",
|
||||
"less": "4.2.0",
|
||||
"less": "4.2.1",
|
||||
"lint-staged": "15.2.10",
|
||||
"mocha": "10.8.2",
|
||||
"prettier": "3.3.3",
|
||||
|
||||
2
prod.js
Normal file
2
prod.js
Normal file
@@ -0,0 +1,2 @@
|
||||
process.env.NODE_ENV = 'production';
|
||||
import('./index.js');
|
||||
154
ui/src/App.jsx
154
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,94 +6,106 @@ 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';
|
||||
|
||||
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 = () => (
|
||||
<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/>}
|
||||
const login = () => (
|
||||
<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'} />
|
||||
<Route name="Login" path={'/login'} component={Login}/>
|
||||
<Redirect from="*" to={'/login'}/>
|
||||
</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';
|
||||
|
||||
24
ui/src/services/rematch/models/demoMode.js
Normal file
24
ui/src/services/rematch/models/demoMode.js
Normal 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);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { provider } from './models/provider';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { jobs } from './models/jobs';
|
||||
import { user } from './models/user';
|
||||
import { demoMode } from './models/demoMode.js';
|
||||
import { init } from '@rematch/core';
|
||||
const middleware = [];
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
@@ -16,6 +17,7 @@ const store = init({
|
||||
models: {
|
||||
notificationAdapter,
|
||||
generalSettings,
|
||||
demoMode,
|
||||
provider,
|
||||
jobs,
|
||||
user,
|
||||
|
||||
@@ -59,7 +59,7 @@ const GeneralSettings = function GeneralSettings() {
|
||||
setWorkingHourFrom(settings?.workingHours?.from);
|
||||
setWorkingHourTo(settings?.workingHours?.to);
|
||||
setScrapingAntProxy(settings?.scrapingAnt?.proxy || 'datacenter');
|
||||
setAnalyticsEnabled(settings?.analytics || false);
|
||||
setAnalyticsEnabled(settings?.analyticsEnabled || false);
|
||||
setDemoMode(settings?.demoMode || false);
|
||||
}
|
||||
|
||||
@@ -109,10 +109,17 @@ const GeneralSettings = function GeneralSettings() {
|
||||
});
|
||||
} catch (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;
|
||||
}
|
||||
throwMessage('Settings stored successfully.', 'success');
|
||||
throwMessage('Settings stored successfully. We will reload your browser in 3 seconds.', 'success');
|
||||
setTimeout(()=>{
|
||||
location.reload();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -120,14 +127,6 @@ const GeneralSettings = function GeneralSettings() {
|
||||
{!loading && (
|
||||
<React.Fragment>
|
||||
<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>
|
||||
<SegmentPart
|
||||
name="Interval"
|
||||
|
||||
@@ -1,88 +1,102 @@
|
||||
import React 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 } 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 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('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [error, setError] = React.useState(null);
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
await dispatch.demoMode.getDemoMode();
|
||||
}
|
||||
|
||||
const history = useHistory();
|
||||
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>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
Login.displayName = 'Login';
|
||||
|
||||
134
yarn.lock
134
yarn.lock
@@ -43,7 +43,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40"
|
||||
integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==
|
||||
@@ -695,14 +695,14 @@
|
||||
dependencies:
|
||||
"@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"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz#c0b6cae9c1b73967f7f9eb2fca9536ba2fad2858"
|
||||
integrity sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==
|
||||
dependencies:
|
||||
"@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"
|
||||
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==
|
||||
@@ -991,33 +991,33 @@
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@douyinfe/semi-animation-react@2.69.2":
|
||||
version "2.69.2"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.69.2.tgz#b47565c64dae7f4e1a7c5a9a21a244d59986b3fd"
|
||||
integrity sha512-N6bdju90nnQdNHmnp5C8n8oqSDqqzgO6rzCPwwb6Ef4+aC/csdU1/Dsdp6JA6QKQ768oHGPT5YJs3QiKSGZZcw==
|
||||
"@douyinfe/semi-animation-react@2.70.1":
|
||||
version "2.70.1"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.70.1.tgz#1fe840336a3a49b3d2ed0bac3479135fe6af1a31"
|
||||
integrity sha512-xiryCQGhjGUZ4/j5WIfjtn9s8fZsTlSxmebbmZK576qm05020YCc92r52g8z1SgXDBluOqs+OLrrWJw9pq6IHg==
|
||||
dependencies:
|
||||
"@douyinfe/semi-animation" "2.69.2"
|
||||
"@douyinfe/semi-animation-styled" "2.69.2"
|
||||
"@douyinfe/semi-animation" "2.70.1"
|
||||
"@douyinfe/semi-animation-styled" "2.70.1"
|
||||
classnames "^2.2.6"
|
||||
|
||||
"@douyinfe/semi-animation-styled@2.69.2":
|
||||
version "2.69.2"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.69.2.tgz#18c16a959c92e908aa4fad521fe7e0fe83296034"
|
||||
integrity sha512-HHHR2qS7BRCtP78qp9N/OL9RWPvoxxRg6uC6kUm8l4t5FCcr0QrdhkzYIpohAVa90BedpTPwhRHhh3aiXfnx9A==
|
||||
"@douyinfe/semi-animation-styled@2.70.1":
|
||||
version "2.70.1"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.70.1.tgz#4f094a90bad134b97c4ebdb7b611a6a7f956f6d7"
|
||||
integrity sha512-6ESi+RruTF6U+PzBKT+5DcK1MCwryu1B9nvZiwXflXRRRzi37OzvDy68g3uo/btcpeUvWN8Oi0G+Lg2t8z3xQQ==
|
||||
|
||||
"@douyinfe/semi-animation@2.69.2":
|
||||
version "2.69.2"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.69.2.tgz#4023340747eb202f5e3b2d48dfd4efea94d815cb"
|
||||
integrity sha512-elut0fb5eKr5pnrZKgaOS97nw+KxkoL4N+tho4u099a3K5GFwzvyzVPOK0ALReCWnO0tSxUbwPpUQxMomG+vKA==
|
||||
"@douyinfe/semi-animation@2.70.1":
|
||||
version "2.70.1"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.70.1.tgz#f200e65d9f234ddc2124bb9b09d99a602dc4280f"
|
||||
integrity sha512-CZBuMsVETxgau3j3jCr6pur3vZk3Sk1BZx4CBaD4EzWx4mXNY3aNRwaNwcuR3TWl0zxLX2HTpLgMPe4X+aW+/g==
|
||||
dependencies:
|
||||
bezier-easing "^2.1.0"
|
||||
|
||||
"@douyinfe/semi-foundation@2.69.2":
|
||||
version "2.69.2"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.69.2.tgz#2a782760511e509410df87e473e956465e8b1a6f"
|
||||
integrity sha512-qiN1uBxEs+ofIAGOw6oF7AgTXDlfgmbl9xYTDsS5D3tSH0p0hjAsqW2d59/lScr3P7dYzxKOXZ6aJrC5TXW3Wg==
|
||||
"@douyinfe/semi-foundation@2.70.1":
|
||||
version "2.70.1"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.70.1.tgz#de5b8ec06ddc94c8e1ba747a3ca14fe9c9ff8e41"
|
||||
integrity sha512-+A6LU5Jqqqgn1BG4n5r8TDKbfT8eSW5XKhGw9eekfnbQkfu5EEgsyZQGo2KKwh7TKUlIc6ntrAH4skviPMqJ6Q==
|
||||
dependencies:
|
||||
"@douyinfe/semi-animation" "2.69.2"
|
||||
"@douyinfe/semi-animation" "2.70.1"
|
||||
"@mdx-js/mdx" "^3.0.1"
|
||||
async-validator "^3.5.0"
|
||||
classnames "^2.2.6"
|
||||
@@ -1031,37 +1031,37 @@
|
||||
remark-gfm "^4.0.0"
|
||||
scroll-into-view-if-needed "^2.2.24"
|
||||
|
||||
"@douyinfe/semi-icons@2.69.2":
|
||||
version "2.69.2"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.69.2.tgz#92ade6402237c1a98d4f39a6d447a874848ea066"
|
||||
integrity sha512-0Wzb4bd5DYZjlcR9JS2Cv5D7LeSApy0TD4BMp8rHRStp9iNIGnB/Fob7OFAz9ZuceJ8nb+IN4uxQyf0GuNh/tA==
|
||||
"@douyinfe/semi-icons@2.70.1":
|
||||
version "2.70.1"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.70.1.tgz#382ce7c498deeeb08ed16088cf21b8a319bda20b"
|
||||
integrity sha512-5So+o2nHp468lyRihwzAbRvo0/PQi0rZdKdepyD8tQEEl6ou00bgie/p9/ZShdrWHrG2a3oyFX77pTsGTbEnNQ==
|
||||
dependencies:
|
||||
classnames "^2.2.6"
|
||||
|
||||
"@douyinfe/semi-illustrations@2.69.2":
|
||||
version "2.69.2"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.69.2.tgz#aac0c5c65c1363c86ab6dcfd702a0d4d9a3a1c23"
|
||||
integrity sha512-rdRB6ZZ2zo2c/e0Hkffq84i0w90BMmhBGskvei8AWDlfiuTJhXL6qZ3ixZiE+fjPQO12mk/QNG+LNv8Tr5yFfQ==
|
||||
"@douyinfe/semi-illustrations@2.70.1":
|
||||
version "2.70.1"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.70.1.tgz#2c6d870960fa609936c4d560434279b2bb439209"
|
||||
integrity sha512-kk87Uf4kPUWe816ywK8OwFafXqL4HWJQgFYE/Tb+LAw4uuGpDQKE0aELlSzR2/QpfUIAsPExqOKNeWvDVSHDZg==
|
||||
|
||||
"@douyinfe/semi-theme-default@2.69.2":
|
||||
version "2.69.2"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.69.2.tgz#6256c55f07e34b8f134e5a7a2f9fe85970387bf3"
|
||||
integrity sha512-pyol1EFUwuErp6Tlw+VLs4nVjnj1PSEC5P3YB0MpIcDWZsH/ixRsgMAGuvWI/+owwZ6KnzRDA0t+XcGm+e17qA==
|
||||
"@douyinfe/semi-theme-default@2.70.1":
|
||||
version "2.70.1"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.70.1.tgz#fcb844849ae70b6a16155ea0a9bbf0f0f63aed21"
|
||||
integrity sha512-urIcebhGXVeVslf26ujSiQ4YQcqUxyvWimP9AM2IXkt6MtsYlVDpdyiIoOyO4IMxHxfMhFAphh2UXIbdlkTPOA==
|
||||
|
||||
"@douyinfe/semi-ui@2.69.2":
|
||||
version "2.69.2"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.69.2.tgz#9f74898cc865fb01c622aa58f6205cbd11f3aa47"
|
||||
integrity sha512-oDI3jLlwugpF8vNx6R+ivwTh1hu7Sr8Yrb3+8nsxNlc9+C+RHA01uu4xGmORwHQdYe2+YRn+kFP8+tu2Ch90Nw==
|
||||
"@douyinfe/semi-ui@2.70.1":
|
||||
version "2.70.1"
|
||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.70.1.tgz#6a863663add0d8b31f048e5a63f99820bc087f72"
|
||||
integrity sha512-irXuCCDr6oUzzv0usqWqSo11LeqNEi+yPaKTAujv8OnugfDG4e084cCXTa5/E6xSFS3UXJEWD7hUe7NB0+FNHA==
|
||||
dependencies:
|
||||
"@dnd-kit/core" "^6.0.8"
|
||||
"@dnd-kit/sortable" "^7.0.2"
|
||||
"@dnd-kit/utilities" "^3.2.1"
|
||||
"@douyinfe/semi-animation" "2.69.2"
|
||||
"@douyinfe/semi-animation-react" "2.69.2"
|
||||
"@douyinfe/semi-foundation" "2.69.2"
|
||||
"@douyinfe/semi-icons" "2.69.2"
|
||||
"@douyinfe/semi-illustrations" "2.69.2"
|
||||
"@douyinfe/semi-theme-default" "2.69.2"
|
||||
"@douyinfe/semi-animation" "2.70.1"
|
||||
"@douyinfe/semi-animation-react" "2.70.1"
|
||||
"@douyinfe/semi-foundation" "2.70.1"
|
||||
"@douyinfe/semi-icons" "2.70.1"
|
||||
"@douyinfe/semi-illustrations" "2.70.1"
|
||||
"@douyinfe/semi-theme-default" "2.70.1"
|
||||
async-validator "^3.5.0"
|
||||
classnames "^2.2.6"
|
||||
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"
|
||||
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
|
||||
|
||||
"@vitejs/plugin-react@4.3.3":
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz#28301ac6d7aaf20b73a418ee5c65b05519b4836c"
|
||||
integrity sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==
|
||||
"@vitejs/plugin-react@4.3.4":
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz#c64be10b54c4640135a5b28a2432330e88ad7c20"
|
||||
integrity sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==
|
||||
dependencies:
|
||||
"@babel/core" "^7.25.2"
|
||||
"@babel/plugin-transform-react-jsx-self" "^7.24.7"
|
||||
"@babel/plugin-transform-react-jsx-source" "^7.24.7"
|
||||
"@babel/core" "^7.26.0"
|
||||
"@babel/plugin-transform-react-jsx-self" "^7.25.9"
|
||||
"@babel/plugin-transform-react-jsx-source" "^7.25.9"
|
||||
"@types/babel__core" "^7.20.5"
|
||||
react-refresh "^0.14.2"
|
||||
|
||||
@@ -1907,10 +1907,10 @@ batch@~0.6.0:
|
||||
resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz"
|
||||
integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
|
||||
|
||||
better-sqlite3@^11.5.0:
|
||||
version "11.5.0"
|
||||
resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-11.5.0.tgz#58faa51e02845a578dd154f0083487132ead0695"
|
||||
integrity sha512-e/6eggfOutzoK0JWiU36jsisdWoHOfN9iWiW/SieKvb7SAa6aGNmBM/UKyp+/wWSXpLlWNN8tCPwoDNPhzUvuQ==
|
||||
better-sqlite3@^11.6.0:
|
||||
version "11.6.0"
|
||||
resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-11.6.0.tgz#e50736956e6fe1c30dc94f1bc94a9c15d63b7b6b"
|
||||
integrity sha512-2J6k/eVxcFYY2SsTxsXrj6XylzHWPxveCn4fKPKZFv/Vqn/Cd7lOuX4d7rGQXT5zL+97MkNL3nSbCrIoe3LkgA==
|
||||
dependencies:
|
||||
bindings "^1.5.0"
|
||||
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"
|
||||
integrity sha512-hyQTX7ezCxl7JqumaWiGsroGWalzh24GedQIgO3vJbkGOZ6ySRAltIYjfxhrq4HszJOySZegotEF7v+haQ75UA==
|
||||
|
||||
highcharts@11.4.8:
|
||||
version "11.4.8"
|
||||
resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-11.4.8.tgz#252e71b81c24ec9f99e756b76dbebd7546e18dda"
|
||||
integrity sha512-5Tke9LuzZszC4osaFisxLIcw7xgNGz4Sy3Jc9pRMV+ydm6sYqsPYdU8ELOgpzGNrbrRNDRBtveoR5xS3SzneEA==
|
||||
highcharts@12.0.1:
|
||||
version "12.0.1"
|
||||
resolved "https://registry.yarnpkg.com/highcharts/-/highcharts-12.0.1.tgz#a8c45938a510fc23ca5380dbfec5f82cd0eee5c9"
|
||||
integrity sha512-86pku0cZnHfEu6uqbbEpU50XKHcAFFXeD4pYvxlVBRDLRDlxpT0WuClgJBuBJZof1ZjFUh1D7IxUxFgt9Epb7Q==
|
||||
|
||||
history@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"
|
||||
integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=
|
||||
|
||||
less@4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmjs.org/less/-/less-4.2.0.tgz"
|
||||
integrity sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==
|
||||
less@4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-4.2.1.tgz#fe4c9848525ab44614c0cf2c00abd8d031bb619a"
|
||||
integrity sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==
|
||||
dependencies:
|
||||
copy-anything "^2.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"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
nanoid@5.0.8:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.8.tgz#7610003f6b3b761b5c244bb342c112c5312512bf"
|
||||
integrity sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==
|
||||
nanoid@5.0.9:
|
||||
version "5.0.9"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.9.tgz#977dcbaac055430ce7b1e19cf0130cea91a20e50"
|
||||
integrity sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==
|
||||
|
||||
nanoid@^3.3.7:
|
||||
version "3.3.7"
|
||||
|
||||
Reference in New Issue
Block a user