增加任务统计

This commit is contained in:
whyour 2026-06-01 18:20:18 +08:00
parent c0b7527148
commit e8ac195c96
18 changed files with 1484 additions and 60 deletions

370
back/api/dashboard.ts Normal file
View File

@ -0,0 +1,370 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { fn, col, where, Op } from 'sequelize';
import { CrontabModel } from '../data/cron';
import { CrontabStatModel } from '../data/cronStats';
import dayjs from 'dayjs';
import os from 'os';
const route = Router();
export default (app: Router) => {
app.use('/dashboard', route);
route.post('/record', async (req: Request, res: Response) => {
try {
const { ref_id, code, elapsed } = req.body;
if (!ref_id) return res.send({ code: 400, message: 'ref_id required' });
const today = dayjs().format('YYYY-MM-DD');
const isSuccess = code === 0 ? 1 : 0;
const isFail = code !== 0 ? 1 : 0;
const elapsedMs = (Number(elapsed) || 0) * 1000;
const existing = await CrontabStatModel.findOne({
where: { ref_id: Number(ref_id), date: today },
});
if (existing) {
await CrontabStatModel.update(
{
run_count: (existing.run_count || 0) + 1,
success_count: (existing.success_count || 0) + isSuccess,
fail_count: (existing.fail_count || 0) + isFail,
total_time: (existing.total_time || 0) + elapsedMs,
max_time: Math.max(existing.max_time || 0, elapsedMs),
},
{ where: { id: existing.id } },
);
} else {
await CrontabStatModel.create({
ref_id: Number(ref_id),
date: today,
run_count: 1,
success_count: isSuccess,
fail_count: isFail,
total_time: elapsedMs,
max_time: elapsedMs,
});
}
res.send({ code: 200 });
} catch (e) {
res.send({ code: 500 });
}
});
route.get(
'/overview',
async (req: Request, res: Response, next: NextFunction) => {
try {
const today = dayjs().format('YYYY-MM-DD');
const [total, enabled, disabled, stats] = await Promise.all([
CrontabModel.count(),
CrontabModel.count({ where: { isDisabled: 0 } }),
CrontabModel.count({ where: { isDisabled: 1 } }),
CrontabStatModel.findOne({
attributes: [
[fn('SUM', col('run_count')), 'total_runs'],
[fn('SUM', col('success_count')), 'total_success'],
[fn('SUM', col('fail_count')), 'total_fail'],
[fn('SUM', col('total_time')), 'total_time'],
],
where: { date: today },
raw: true,
}),
]);
const row = stats as any;
const totalRuns = Number(row?.total_runs) || 0;
const totalSuccess = Number(row?.total_success) || 0;
const totalFail = Number(row?.total_fail) || 0;
const totalTime = Number(row?.total_time) || 0;
res.send({
code: 200,
data: {
total,
enabled,
disabled,
todayRuns: totalRuns,
todaySuccess: totalSuccess,
todayFail: totalFail,
successRate: totalRuns > 0 ? ((totalSuccess / totalRuns) * 100).toFixed(1) : '0',
avgTime: totalRuns > 0 ? Math.round(totalTime / totalRuns) : 0,
},
});
} catch (e) {
next(e);
}
},
);
route.get(
'/trend',
async (req: Request, res: Response, next: NextFunction) => {
try {
const days = parseInt(req.query.days as string) || 7;
const dates: string[] = [];
for (let i = days - 1; i >= 0; i--) {
dates.push(dayjs().subtract(i, 'day').format('YYYY-MM-DD'));
}
const rows = (await CrontabStatModel.findAll({
attributes: [
'date',
[fn('SUM', col('run_count')), 'total_runs'],
[fn('SUM', col('success_count')), 'total_success'],
[fn('SUM', col('fail_count')), 'total_fail'],
],
where: {
date: { [Op.in]: dates },
},
group: ['date'],
order: [['date', 'ASC']],
raw: true,
})) as any[];
const dataMap: Record<string, any> = {};
rows.forEach((r: any) => {
dataMap[r.date] = {
total: Number(r.total_runs) || 0,
success: Number(r.total_success) || 0,
fail: Number(r.total_fail) || 0,
};
});
const data = dates.map((d) => ({
date: dayjs(d).format('MM-DD'),
...(dataMap[d] || { total: 0, success: 0, fail: 0 }),
}));
res.send({ code: 200, data });
} catch (e) {
next(e);
}
},
);
route.get(
'/top-time',
async (req: Request, res: Response, next: NextFunction) => {
try {
const today = dayjs().format('YYYY-MM-DD');
const rows = (await CrontabStatModel.findAll({
attributes: [
'ref_id',
[fn('SUM', col('total_time')), 'total_time'],
[fn('SUM', col('run_count')), 'run_count'],
[fn('MAX', col('max_time')), 'max_time'],
],
where: { date: today, run_count: { [Op.gt]: 0 } },
group: ['ref_id'],
order: [[fn('SUM', col('total_time')), 'DESC']],
limit: 5,
raw: true,
})) as any[];
const ids = rows.map((r) => Number(r.ref_id));
const crons = await CrontabModel.findAll({
where: { id: { [Op.in]: ids } },
raw: true,
});
const nameMap: Record<number, string> = {};
crons.forEach((c: any) => { nameMap[c.id] = c.name || c.command; });
const data = rows.map((r: any, i) => ({
rank: i + 1,
name: nameMap[Number(r.ref_id)] || `任务#${r.ref_id}`,
avgTime: Math.round(Number(r.total_time) / Number(r.run_count)),
maxTime: Number(r.max_time),
}));
res.send({ code: 200, data });
} catch (e) {
next(e);
}
},
);
route.get(
'/top-count',
async (req: Request, res: Response, next: NextFunction) => {
try {
const today = dayjs().format('YYYY-MM-DD');
const rows = (await CrontabStatModel.findAll({
attributes: [
'ref_id',
[fn('SUM', col('run_count')), 'run_count'],
[fn('SUM', col('total_time')), 'total_time'],
[fn('SUM', col('success_count')), 'success_count'],
],
where: { date: today, run_count: { [Op.gt]: 0 } },
group: ['ref_id'],
order: [[fn('SUM', col('run_count')), 'DESC']],
limit: 5,
raw: true,
})) as any[];
const ids = rows.map((r) => Number(r.ref_id));
const crons = await CrontabModel.findAll({
where: { id: { [Op.in]: ids } },
raw: true,
});
const nameMap: Record<number, string> = {};
crons.forEach((c: any) => { nameMap[c.id] = c.name || c.command; });
const data = rows.map((r: any, i) => ({
rank: i + 1,
name: nameMap[Number(r.ref_id)] || `任务#${r.ref_id}`,
runCount: Number(r.run_count),
avgTime: Math.round(Number(r.total_time) / Number(r.run_count)),
successRate:
Number(r.run_count) > 0
? ((Number(r.success_count) / Number(r.run_count)) * 100).toFixed(1)
: '0',
}));
res.send({ code: 200, data });
} catch (e) {
next(e);
}
},
);
route.get(
'/runtime',
async (req: Request, res: Response, next: NextFunction) => {
try {
const runningCrons = await CrontabModel.findAll({
where: {
status: 0, // running
},
raw: true,
});
const queuedCrons = await CrontabModel.findAll({
where: {
status: 3, // queued
},
raw: true,
});
const running = runningCrons.map((c: any) => ({
id: c.id,
name: c.name || c.command || `任务#${c.id}`,
pid: c.pid,
elapsed: c.last_execution_time
? Math.floor((Date.now() / 1000) - c.last_execution_time)
: 0,
logPath: c.log_path,
}));
const dayAgo = dayjs().subtract(24, 'hour').unix();
const idleTasks = await CrontabModel.findAll({
where: {
isDisabled: 0,
status: 1,
last_execution_time: { [Op.lt]: dayAgo },
},
order: [['last_execution_time', 'ASC']],
limit: 5,
raw: true,
});
res.send({
code: 200,
data: {
runningCount: running.length,
queuedCount: queuedCrons.length,
running,
idleTasks: idleTasks.map((c: any) => ({
id: c.id,
name: c.name || c.command || `任务#${c.id}`,
lastRun: c.last_execution_time
? dayjs.unix(c.last_execution_time).format('MM-DD HH:mm')
: '-',
})),
},
});
} catch (e) {
next(e);
}
},
);
route.get(
'/labels',
async (req: Request, res: Response, next: NextFunction) => {
try {
const today = dayjs().format('YYYY-MM-DD');
const [crons, stats] = (await Promise.all([
CrontabModel.findAll({ where: { isDisabled: 0 }, raw: true }),
CrontabStatModel.findAll({ where: { date: today }, raw: true }),
])) as any[];
const statMap: Record<number, any> = {};
stats.forEach((s: any) => { statMap[s.ref_id] = s; });
const labelMap: Record<string, { count: number; runs: number; success: number; totalTime: number }> = {};
crons.forEach((c: any) => {
let rawLabels = c.labels;
if (typeof rawLabels === 'string') rawLabels = JSON.parse(rawLabels);
const labels: string[] = Array.isArray(rawLabels) && rawLabels.length > 0 ? rawLabels : ['未分类'];
const st = statMap[c.id];
labels.forEach((label: string) => {
if (!labelMap[label]) labelMap[label] = { count: 0, runs: 0, success: 0, totalTime: 0 };
labelMap[label].count += 1;
if (st) {
labelMap[label].runs += Number(st.run_count) || 0;
labelMap[label].success += Number(st.success_count) || 0;
labelMap[label].totalTime += Number(st.total_time) || 0;
}
});
});
const data = Object.entries(labelMap)
.map(([label, v]) => ({
label,
count: v.count,
todayRuns: v.runs,
successRate: v.runs > 0 ? ((v.success / v.runs) * 100).toFixed(1) : '0',
avgTime: v.runs > 0 ? Math.round(v.totalTime / v.runs) : 0,
}))
.sort((a, b) => b.todayRuns - a.todayRuns);
res.send({ code: 200, data });
} catch (e) {
next(e);
}
},
);
route.get(
'/system',
async (req: Request, res: Response, next: NextFunction) => {
try {
const memUsage = process.memoryUsage();
res.send({
code: 200,
data: {
platform: os.platform(),
uptime: Math.floor(os.uptime()),
memTotal: os.totalmem(),
memFree: os.freemem(),
memUsagePercent: ((1 - os.freemem() / os.totalmem()) * 100).toFixed(1),
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
loadAvg: os.loadavg().map((v) => Number(v.toFixed(2))),
cpus: os.cpus().length,
},
});
} catch (e) {
next(e);
}
},
);
};

View File

@ -10,6 +10,7 @@ import dependence from './dependence';
import system from './system'; import system from './system';
import subscription from './subscription'; import subscription from './subscription';
import update from './update'; import update from './update';
import dashboard from './dashboard';
import health from './health'; import health from './health';
export default () => { export default () => {
@ -25,6 +26,7 @@ export default () => {
system(app); system(app);
subscription(app); subscription(app);
update(app); update(app);
dashboard(app);
health(app); health(app);
return app; return app;

71
back/data/cronStats.ts Normal file
View File

@ -0,0 +1,71 @@
import { DataTypes, Model } from 'sequelize';
import { sequelize } from '.';
export class CrontabStat {
id?: number;
ref_id!: number;
date!: string;
run_count?: number;
success_count?: number;
fail_count?: number;
total_time?: number;
max_time?: number;
constructor(options: CrontabStat) {
this.id = options.id;
this.ref_id = options.ref_id;
this.date = options.date;
this.run_count = options.run_count || 0;
this.success_count = options.success_count || 0;
this.fail_count = options.fail_count || 0;
this.total_time = options.total_time || 0;
this.max_time = options.max_time || 0;
}
}
export interface CrontabStatInstance extends Model<CrontabStat, CrontabStat>, CrontabStat {}
export const CrontabStatModel = sequelize.define<CrontabStatInstance>(
'CrontabStat',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
ref_id: {
type: DataTypes.NUMBER,
allowNull: false,
},
date: {
type: DataTypes.STRING,
allowNull: false,
},
run_count: {
type: DataTypes.NUMBER,
defaultValue: 0,
},
success_count: {
type: DataTypes.NUMBER,
defaultValue: 0,
},
fail_count: {
type: DataTypes.NUMBER,
defaultValue: 0,
},
total_time: {
type: DataTypes.NUMBER,
defaultValue: 0,
},
max_time: {
type: DataTypes.NUMBER,
defaultValue: 0,
},
},
{
indexes: [
{ unique: true, fields: ['ref_id', 'date'] },
{ fields: ['date'] },
],
},
);

View File

@ -24,7 +24,7 @@ export interface AppToken {
expiration: number; expiration: number;
} }
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs' | 'system'; export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs' | 'system' | 'dashboard';
export interface AppInstance extends Model<App, App>, App {} export interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', { export const AppModel = sequelize.define<AppInstance>('App', {

View File

@ -6,6 +6,7 @@ import { AppModel } from '../data/open';
import { SystemModel } from '../data/system'; import { SystemModel } from '../data/system';
import { SubscriptionModel } from '../data/subscription'; import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView'; import { CrontabViewModel } from '../data/cronView';
import { CrontabStatModel } from '../data/cronStats';
import { sequelize } from '../data'; import { sequelize } from '../data';
export default async () => { export default async () => {
@ -17,6 +18,7 @@ export default async () => {
await EnvModel.sync(); await EnvModel.sync();
await SubscriptionModel.sync(); await SubscriptionModel.sync();
await CrontabViewModel.sync(); await CrontabViewModel.sync();
await CrontabStatModel.sync();
// 初始化新增字段 // 初始化新增字段
const migrations = [ const migrations = [

View File

@ -36,10 +36,15 @@ export default async () => {
if (!systemApp) { if (!systemApp) {
systemApp = await AppModel.create({ systemApp = await AppModel.create({
name: 'system', name: 'system',
scopes: ['crons', 'system'], scopes: ['crons', 'system', 'dashboard'],
client_id: createRandomString(12, 12), client_id: createRandomString(12, 12),
client_secret: createRandomString(24, 24), client_secret: createRandomString(24, 24),
}); });
} else if (!systemApp.scopes.includes('dashboard')) {
await AppModel.update(
{ scopes: [...systemApp.scopes, 'dashboard'] },
{ where: { name: 'system' } },
);
} }
const [systemConfig] = await SystemModel.findOrCreate({ const [systemConfig] = await SystemModel.findOrCreate({
where: { type: AuthDataType.systemConfig }, where: { type: AuthDataType.systemConfig },

View File

@ -573,7 +573,6 @@ export default class CronService {
JSON.stringify(params), JSON.stringify(params),
code, code,
); );
// Close the stream after task completion
await logStreamManager.closeStream(absolutePath); await logStreamManager.closeStream(absolutePath);
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined }, { status: CrontabStatus.idle, pid: undefined },

View File

@ -73,12 +73,16 @@
} }
}, },
"dependencies": { "dependencies": {
"@ant-design/plots": "^2.6.8",
"@bufbuild/protobuf": "^2.10.0",
"@grpc/grpc-js": "^1.14.0", "@grpc/grpc-js": "^1.14.0",
"@grpc/proto-loader": "^0.8.0", "@grpc/proto-loader": "^0.8.0",
"@keyv/sqlite": "^4.0.1",
"@otplib/preset-default": "^12.0.1", "@otplib/preset-default": "^12.0.1",
"body-parser": "^1.20.3", "body-parser": "^1.20.3",
"celebrate": "^15.0.3", "celebrate": "^15.0.3",
"chokidar": "^4.0.1", "chokidar": "^4.0.1",
"compression": "^1.7.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^5.4.0", "cron-parser": "^5.4.0",
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
@ -88,51 +92,48 @@
"express-jwt": "^8.4.1", "express-jwt": "^8.4.1",
"express-rate-limit": "^7.4.1", "express-rate-limit": "^7.4.1",
"express-urlrewrite": "^2.0.3", "express-urlrewrite": "^2.0.3",
"undici": "^7.9.0", "helmet": "^8.1.0",
"hpagent": "^1.2.0", "hpagent": "^1.2.0",
"http-proxy-middleware": "^3.0.3", "http-proxy-middleware": "^3.0.3",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"ip2region": "2.3.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"keyv": "^5.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "2.1.1", "multer": "2.1.1",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^8.0.1", "nodemailer": "^8.0.1",
"p-queue-cjs": "7.3.4", "p-queue-cjs": "7.3.4",
"@bufbuild/protobuf": "^2.10.0", "proper-lockfile": "^4.1.2",
"ps-tree": "^1.2.0", "ps-tree": "^1.2.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"request-ip": "3.3.0",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3", "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
"toad-scheduler": "^3.0.1", "toad-scheduler": "^3.0.1",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"undici": "^7.9.0",
"uuid": "^11.0.3", "uuid": "^11.0.3",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0", "winston-daily-rotate-file": "^5.0.0"
"request-ip": "3.3.0",
"ip2region": "2.3.0",
"keyv": "^5.2.3",
"@keyv/sqlite": "^4.0.1",
"proper-lockfile": "^4.1.2",
"compression": "^1.7.4",
"helmet": "^8.1.0"
}, },
"devDependencies": { "devDependencies": {
"moment": "2.30.1",
"@ant-design/icons": "^5.0.1", "@ant-design/icons": "^5.0.1",
"@ant-design/pro-layout": "6.38.22", "@ant-design/pro-layout": "6.38.22",
"@codemirror/view": "6.39.16",
"@codemirror/state": "6.5.4", "@codemirror/state": "6.5.4",
"@codemirror/view": "6.39.16",
"@monaco-editor/react": "4.2.1", "@monaco-editor/react": "4.2.1",
"@react-hook/resize-observer": "^2.0.2", "@react-hook/resize-observer": "^2.0.2",
"react-router-dom": "6.26.1",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/compression": "^1.7.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4", "@types/express-jwt": "^6.0.4",
"@types/file-saver": "2.0.2", "@types/file-saver": "2.0.2",
"@types/helmet": "^4.0.0",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185", "@types/lodash": "^4.14.185",
@ -140,17 +141,17 @@
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
"@types/node-schedule": "^1.3.2", "@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/proper-lockfile": "^4.1.4",
"@types/ps-tree": "^1.1.6",
"@types/qrcode.react": "^1.0.2", "@types/qrcode.react": "^1.0.2",
"@types/react": "^18.0.20", "@types/react": "^18.0.20",
"@types/react-copy-to-clipboard": "^5.0.4", "@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/request-ip": "0.0.41",
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@types/request-ip": "0.0.41",
"@types/proper-lockfile": "^4.1.4",
"@types/ps-tree": "^1.1.6",
"@uiw/codemirror-extensions-langs": "^4.21.9", "@uiw/codemirror-extensions-langs": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9", "@uiw/react-codemirror": "^4.21.9",
"@umijs/max": "^4.4.4", "@umijs/max": "^4.4.4",
@ -162,9 +163,9 @@
"axios": "^1.4.0", "axios": "^1.4.0",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"react-hotkeys-hook": "^4.6.1",
"file-saver": "2.0.2", "file-saver": "2.0.2",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"moment": "2.30.1",
"monaco-editor": "0.33.0", "monaco-editor": "0.33.0",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"prettier": "^2.5.1", "prettier": "^2.5.1",
@ -180,7 +181,9 @@
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-hotkeys-hook": "^4.6.1",
"react-intl-universal": "^2.12.0", "react-intl-universal": "^2.12.0",
"react-router-dom": "6.26.1",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
@ -188,8 +191,6 @@
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "5.2.2", "typescript": "5.2.2",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0", "virtualizedtableforantd4": "1.3.0"
"@types/compression": "^1.7.2",
"@types/helmet": "^4.0.0"
} }
} }

View File

@ -1,11 +1,18 @@
lockfileVersion: '6.0' lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides: overrides:
sqlite3: git+https://github.com/whyour/node-sqlite3.git#v1.0.3 sqlite3: git+https://github.com/whyour/node-sqlite3.git#v1.0.3
'@codemirror/state': 6.5.4 '@codemirror/state': 6.5.4
'@codemirror/view': 6.39.16 '@codemirror/view': 6.39.16
dependencies: dependencies:
'@ant-design/plots':
specifier: ^2.6.8
version: 2.6.8(react-dom@18.3.1)(react@18.3.1)
'@bufbuild/protobuf': '@bufbuild/protobuf':
specifier: ^2.10.0 specifier: ^2.10.0
version: 2.10.0 version: 2.10.0
@ -384,6 +391,22 @@ packages:
resolution: {integrity: sha512-0vr5GCwM7xlAl6NxG1lPbABO+SYioNJL3HVy2FA8wTlsIMoZvQwcwsxTw6eLQCiN9V2UQ8kBtfz8DW8utVVE5w==} resolution: {integrity: sha512-0vr5GCwM7xlAl6NxG1lPbABO+SYioNJL3HVy2FA8wTlsIMoZvQwcwsxTw6eLQCiN9V2UQ8kBtfz8DW8utVVE5w==}
dev: true dev: true
/@ant-design/charts-util@0.0.3(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-x1H7UT6t4dXAyGRoHqlOnEsEqBSTANFGTZEAMI0CWYhYUpp13n0o9grl9oPtoL6FEQMjUBTY+zGJKlHkz8smMw==}
peerDependencies:
react: '>=16.8.4 || 18'
react-dom: '>=16.8.4 || 18'
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
dependencies:
lodash: 4.17.21
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@ant-design/colors@6.0.0: /@ant-design/colors@6.0.0:
resolution: {integrity: sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==} resolution: {integrity: sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==}
dependencies: dependencies:
@ -469,6 +492,27 @@ packages:
resolution: {integrity: sha512-MLm1FUpg02fP615ShQnCUN9la2E4RylDxKyolkGqAWTIHO4HyGM0A5x71AMALEyP/bC+UEEWBGSQ+D4/8hQ+ww==} resolution: {integrity: sha512-MLm1FUpg02fP615ShQnCUN9la2E4RylDxKyolkGqAWTIHO4HyGM0A5x71AMALEyP/bC+UEEWBGSQ+D4/8hQ+ww==}
dev: true dev: true
/@ant-design/plots@2.6.8(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-QsunUs2d5rbq/1BwVhga/siA5H50OaG23YopMYwPD4sPsza6NQzPQ8FM3elNIsD/BIk298tihqX1cJ/MmvVJbQ==}
peerDependencies:
react: '>=16.8.4 || 18'
react-dom: '>=16.8.4 || 18'
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
dependencies:
'@ant-design/charts-util': 0.0.3(react-dom@18.3.1)(react@18.3.1)
'@antv/event-emitter': 0.1.3
'@antv/g': 6.3.1
'@antv/g2': 5.4.8
'@antv/g2-extension-plot': 0.2.2
lodash: 4.17.21
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@ant-design/pro-card@2.9.2(antd@4.24.16)(react-dom@18.3.1)(react@18.3.1): /@ant-design/pro-card@2.9.2(antd@4.24.16)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-mKOmNb7jc3Pz41RrPY7EFKRWBjLdN4tp9yzmRkS2g8K7P3pW435f7Ip6rc+58FWDzbZa8lElTGPxAoFB/dq7LA==} resolution: {integrity: sha512-mKOmNb7jc3Pz41RrPY7EFKRWBjLdN4tp9yzmRkS2g8K7P3pW435f7Ip6rc+58FWDzbZa8lElTGPxAoFB/dq7LA==}
peerDependencies: peerDependencies:
@ -903,6 +947,184 @@ packages:
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
dev: true dev: true
/@antv/component@2.1.11:
resolution: {integrity: sha512-dTdz8VAd3rpjOaGEZTluz82mtzrP4XCtNlNQyrxY7VNRNcjtvpTLDn57bUL2lRu1T+iklKvgbE2llMriWkq9vQ==}
dependencies:
'@antv/g': 6.3.1
'@antv/scale': 0.4.16
'@antv/util': 3.3.11
svg-path-parser: 1.1.0
dev: false
/@antv/coord@0.4.7:
resolution: {integrity: sha512-UTbrMLhwJUkKzqJx5KFnSRpU3BqrdLORJbwUbHK2zHSCT3q3bjcFA//ZYLVfIlwqFDXp/hzfMyRtp0c77A9ZVA==}
dependencies:
'@antv/scale': 0.4.16
'@antv/util': 2.0.17
gl-matrix: 3.4.4
dev: false
/@antv/event-emitter@0.1.3:
resolution: {integrity: sha512-4ddpsiHN9Pd4UIlWuKVK1C4IiZIdbwQvy9i7DUSI3xNJ89FPUFt8lxDYj8GzzfdllV0NkJTRxnG+FvLk0llidg==}
dev: false
/@antv/expr@1.0.2:
resolution: {integrity: sha512-vrfdmPHkTuiS5voVutKl2l06w1ihBh9A8SFdQPEE+2KMVpkymzGOF1eWpfkbGZ7tiFE15GodVdhhHomD/hdIwg==}
dev: false
/@antv/g-canvas@2.2.0:
resolution: {integrity: sha512-h7zVBBo2aO64DuGKvq9sG+yTU3sCUb9DALCVm7nz8qGPs8hhLuFOkKPEzUDNfNYZGJUGzY8UDtJ3QRGRFcvEQg==}
dependencies:
'@antv/g-lite': 2.7.0
'@antv/g-math': 3.1.0
'@antv/util': 3.3.11
'@babel/runtime': 7.28.6
gl-matrix: 3.4.4
tslib: 2.8.1
dev: false
/@antv/g-lite@2.7.0:
resolution: {integrity: sha512-uSzgHYa5bwR5L2Au7/5tsOhFmXKZKLPBH90+Q9bP9teVs5VT4kOAi0isPSpDI8uhdDC2/VrfTWu5K9HhWI6FWw==}
dependencies:
'@antv/g-math': 3.1.0
'@antv/util': 3.3.11
'@antv/vendor': 1.0.11
'@babel/runtime': 7.28.6
eventemitter3: 5.0.1
gl-matrix: 3.4.4
tslib: 2.8.1
dev: false
/@antv/g-math@3.1.0:
resolution: {integrity: sha512-DtN1Gj/yI0UiK18nSBsZX8RK0LszGwqfb+cBYWgE+ddyTm8dZnW4tPUhV7QXePsS6/A5hHC+JFpAAK7OEGo5ZQ==}
dependencies:
'@antv/util': 3.3.11
'@babel/runtime': 7.28.6
gl-matrix: 3.4.4
tslib: 2.8.1
dev: false
/@antv/g-plugin-dragndrop@2.1.1:
resolution: {integrity: sha512-+aesDUJVQDs6UJ2bOBbDlaGAPCfHmU0MbrMTlQlfpwNplWueqtgVAZ3L57oZ2ZGHRWUHiRwZGPjXMBM3O2LELw==}
dependencies:
'@antv/g-lite': 2.7.0
'@antv/util': 3.3.11
'@babel/runtime': 7.28.6
tslib: 2.8.1
dev: false
/@antv/g2-extension-plot@0.2.2:
resolution: {integrity: sha512-KJXCXO7as+h0hDqirGXf1omrNuYzQmY3VmBmp7lIvkepbQ7sz3pPwy895r1FWETGF3vTk5UeFcAF5yzzBHWgbw==}
dependencies:
'@antv/g2': 5.4.8
'@antv/util': 3.3.11
'@antv/vendor': 1.0.11
dev: false
/@antv/g2@5.4.8:
resolution: {integrity: sha512-IvgIpwmT4M5/QAd3Mn2WiHIDeBqFJ4WA2gcZhRRSZuZ2KmgCqZWZwwIT0hc+kIGxwYeDoCQqf//t6FMVu3ryBg==}
dependencies:
'@antv/component': 2.1.11
'@antv/coord': 0.4.7
'@antv/event-emitter': 0.1.3
'@antv/expr': 1.0.2
'@antv/g': 6.3.1
'@antv/g-canvas': 2.2.0
'@antv/g-plugin-dragndrop': 2.1.1
'@antv/scale': 0.5.2
'@antv/util': 3.3.11
'@antv/vendor': 1.0.11
flru: 1.0.2
pdfast: 0.2.0
dev: false
/@antv/g@6.3.1:
resolution: {integrity: sha512-WYEKqy86LHB2PzTmrZXrIsIe+3Epeds2f68zceQ+BJtRoGki7Sy4IhlC8LrUMztgfT1t3d/0L745NWZwITroKA==}
dependencies:
'@antv/g-lite': 2.7.0
'@antv/util': 3.3.11
'@babel/runtime': 7.28.6
gl-matrix: 3.4.4
html2canvas: 1.4.1
dev: false
/@antv/scale@0.4.16:
resolution: {integrity: sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==}
dependencies:
'@antv/util': 3.3.11
color-string: 1.9.1
fecha: 4.2.3
dev: false
/@antv/scale@0.5.2:
resolution: {integrity: sha512-rTHRAwvpHWC5PGZF/mJ2ZuTDqwwvVBDRph0Uu5PV9BXwzV7K8+9lsqGJ+XHVLxe8c6bKog5nlzvV/dcYb0d5Ow==}
dependencies:
'@antv/util': 3.3.11
color-string: 1.9.1
fecha: 4.2.3
dev: false
/@antv/util@2.0.17:
resolution: {integrity: sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==}
dependencies:
csstype: 3.2.3
tslib: 2.8.1
dev: false
/@antv/util@3.3.11:
resolution: {integrity: sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==}
dependencies:
fast-deep-equal: 3.1.3
gl-matrix: 3.4.4
tslib: 2.8.1
dev: false
/@antv/vendor@1.0.11:
resolution: {integrity: sha512-LmhPEQ+aapk3barntaiIxJ5VHno/Tyab2JnfdcPzp5xONh/8VSfed4bo/9xKo5HcUAEydko38vYLfj6lJliLiw==}
dependencies:
'@types/d3-array': 3.2.2
'@types/d3-color': 3.1.3
'@types/d3-dispatch': 3.0.7
'@types/d3-dsv': 3.0.7
'@types/d3-ease': 3.0.2
'@types/d3-fetch': 3.0.7
'@types/d3-force': 3.0.10
'@types/d3-format': 3.0.4
'@types/d3-geo': 3.1.0
'@types/d3-hierarchy': 3.1.7
'@types/d3-interpolate': 3.0.4
'@types/d3-path': 3.1.1
'@types/d3-quadtree': 3.0.6
'@types/d3-random': 3.0.3
'@types/d3-scale': 4.0.9
'@types/d3-scale-chromatic': 3.1.0
'@types/d3-shape': 3.1.8
'@types/d3-time': 3.0.4
'@types/d3-timer': 3.0.2
d3-array: 3.2.4
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-dsv: 3.0.1
d3-ease: 3.0.1
d3-fetch: 3.0.1
d3-force: 3.0.0
d3-force-3d: 3.0.6
d3-format: 3.1.2
d3-geo: 3.1.1
d3-geo-projection: 4.0.0
d3-hierarchy: 3.1.2
d3-interpolate: 3.0.1
d3-path: 3.1.0
d3-quadtree: 3.0.1
d3-random: 3.0.1
d3-regression: 1.3.10
d3-scale: 4.0.2
d3-scale-chromatic: 3.1.0
d3-shape: 3.2.0
d3-time: 3.1.0
d3-timer: 3.0.1
dev: false
/@babel/code-frame@7.26.2: /@babel/code-frame@7.26.2:
resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@ -1351,7 +1573,6 @@ packages:
/@babel/runtime@7.28.6: /@babel/runtime@7.28.6:
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dev: true
/@babel/template@7.25.9: /@babel/template@7.25.9:
resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==}
@ -1409,8 +1630,8 @@ packages:
resolution: {integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==} resolution: {integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==}
peerDependencies: peerDependencies:
'@codemirror/language': ^6.0.0 '@codemirror/language': ^6.0.0
'@codemirror/state': ^6.0.0 '@codemirror/state': 6.5.4
'@codemirror/view': ^6.0.0 '@codemirror/view': 6.39.16
'@lezer/common': ^1.0.0 '@lezer/common': ^1.0.0
dependencies: dependencies:
'@codemirror/language': 6.10.6 '@codemirror/language': 6.10.6
@ -3666,8 +3887,8 @@ packages:
peerDependencies: peerDependencies:
'@codemirror/autocomplete': ^6.0.0 '@codemirror/autocomplete': ^6.0.0
'@codemirror/language': ^6.0.0 '@codemirror/language': ^6.0.0
'@codemirror/state': ^6.0.0 '@codemirror/state': 6.5.4
'@codemirror/view': ^6.0.0 '@codemirror/view': 6.39.16
'@lezer/common': ^1.0.0 '@lezer/common': ^1.0.0
'@lezer/highlight': ^1.0.0 '@lezer/highlight': ^1.0.0
'@lezer/lr': ^1.0.0 '@lezer/lr': ^1.0.0
@ -3686,8 +3907,8 @@ packages:
peerDependencies: peerDependencies:
'@codemirror/autocomplete': ^6.0.0 '@codemirror/autocomplete': ^6.0.0
'@codemirror/language': ^6.0.0 '@codemirror/language': ^6.0.0
'@codemirror/state': ^6.0.0 '@codemirror/state': 6.5.4
'@codemirror/view': ^6.0.0 '@codemirror/view': 6.39.16
'@lezer/common': ^1.0.0 '@lezer/common': ^1.0.0
'@lezer/highlight': ^1.0.0 '@lezer/highlight': ^1.0.0
'@lezer/lr': ^1.0.0 '@lezer/lr': ^1.0.0
@ -3718,8 +3939,8 @@ packages:
'@codemirror/lang-html': ^6.2.0 '@codemirror/lang-html': ^6.2.0
'@codemirror/lang-javascript': ^6.1.1 '@codemirror/lang-javascript': ^6.1.1
'@codemirror/language': ^6.0.0 '@codemirror/language': ^6.0.0
'@codemirror/state': ^6.0.0 '@codemirror/state': 6.5.4
'@codemirror/view': ^6.0.0 '@codemirror/view': 6.39.16
'@lezer/common': ^1.0.0 '@lezer/common': ^1.0.0
'@lezer/highlight': ^1.0.0 '@lezer/highlight': ^1.0.0
'@lezer/javascript': ^1.2.0 '@lezer/javascript': ^1.2.0
@ -4082,6 +4303,92 @@ packages:
'@types/node': 17.0.45 '@types/node': 17.0.45
dev: true dev: true
/@types/d3-array@3.2.2:
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
dev: false
/@types/d3-color@3.1.3:
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
dev: false
/@types/d3-dispatch@3.0.7:
resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==}
dev: false
/@types/d3-dsv@3.0.7:
resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
dev: false
/@types/d3-ease@3.0.2:
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
dev: false
/@types/d3-fetch@3.0.7:
resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
dependencies:
'@types/d3-dsv': 3.0.7
dev: false
/@types/d3-force@3.0.10:
resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
dev: false
/@types/d3-format@3.0.4:
resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
dev: false
/@types/d3-geo@3.1.0:
resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
dependencies:
'@types/geojson': 7946.0.16
dev: false
/@types/d3-hierarchy@3.1.7:
resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
dev: false
/@types/d3-interpolate@3.0.4:
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
dependencies:
'@types/d3-color': 3.1.3
dev: false
/@types/d3-path@3.1.1:
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
dev: false
/@types/d3-quadtree@3.0.6:
resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
dev: false
/@types/d3-random@3.0.3:
resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
dev: false
/@types/d3-scale-chromatic@3.1.0:
resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
dev: false
/@types/d3-scale@4.0.9:
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
dependencies:
'@types/d3-time': 3.0.4
dev: false
/@types/d3-shape@3.1.8:
resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
dependencies:
'@types/d3-path': 3.1.1
dev: false
/@types/d3-time@3.0.4:
resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
dev: false
/@types/d3-timer@3.0.2:
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
dev: false
/@types/debug@4.1.12: /@types/debug@4.1.12:
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
dependencies: dependencies:
@ -4124,6 +4431,10 @@ packages:
resolution: {integrity: sha512-xbqnZmGrCEqi/KUzOkeUSe77p7APvLuyellGaAoeww3CHJ1AbjQWjPSCFtKIzZn8L7LpEax4NXnC+gfa6nM7IA==} resolution: {integrity: sha512-xbqnZmGrCEqi/KUzOkeUSe77p7APvLuyellGaAoeww3CHJ1AbjQWjPSCFtKIzZn8L7LpEax4NXnC+gfa6nM7IA==}
dev: true dev: true
/@types/geojson@7946.0.16:
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
dev: false
/@types/graceful-fs@4.1.9: /@types/graceful-fs@4.1.9:
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
dependencies: dependencies:
@ -4539,8 +4850,8 @@ packages:
'@codemirror/language': '>=6.0.0' '@codemirror/language': '>=6.0.0'
'@codemirror/lint': '>=6.0.0' '@codemirror/lint': '>=6.0.0'
'@codemirror/search': '>=6.0.0' '@codemirror/search': '>=6.0.0'
'@codemirror/state': '>=6.0.0' '@codemirror/state': 6.5.4
'@codemirror/view': '>=6.0.0' '@codemirror/view': 6.39.16
dependencies: dependencies:
'@codemirror/autocomplete': 6.20.1 '@codemirror/autocomplete': 6.20.1
'@codemirror/commands': 6.7.1 '@codemirror/commands': 6.7.1
@ -4599,9 +4910,9 @@ packages:
resolution: {integrity: sha512-caYKGV6TfGLRV1HHD3p0G3FiVzKL1go7wes5XT2nWjB0+dTdyzyb81MKRSacptgZcotujfNO6QXn65uhETRAMw==} resolution: {integrity: sha512-caYKGV6TfGLRV1HHD3p0G3FiVzKL1go7wes5XT2nWjB0+dTdyzyb81MKRSacptgZcotujfNO6QXn65uhETRAMw==}
peerDependencies: peerDependencies:
'@babel/runtime': '>=7.11.0' '@babel/runtime': '>=7.11.0'
'@codemirror/state': '>=6.0.0' '@codemirror/state': 6.5.4
'@codemirror/theme-one-dark': '>=6.0.0' '@codemirror/theme-one-dark': '>=6.0.0'
'@codemirror/view': '>=6.0.0' '@codemirror/view': 6.39.16
codemirror: '>=6.0.0' codemirror: '>=6.0.0'
react: '>=16.8.0 || 18' react: '>=16.8.0 || 18'
react-dom: '>=16.8.0 || 18' react-dom: '>=16.8.0 || 18'
@ -5390,6 +5701,7 @@ packages:
/agentkeepalive@4.6.0: /agentkeepalive@4.6.0:
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
requiresBuild: true
dependencies: dependencies:
humanize-ms: 1.2.1 humanize-ms: 1.2.1
dev: false dev: false
@ -6018,6 +6330,11 @@ packages:
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
dev: true dev: true
/base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
dev: false
/base64-js@1.5.1: /base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@ -6554,7 +6871,6 @@ packages:
/commander@7.2.0: /commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
dev: true
/commander@8.3.0: /commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
@ -6873,6 +7189,12 @@ packages:
postcss-selector-parser: 6.1.2 postcss-selector-parser: 6.1.2
dev: true dev: true
/css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
dependencies:
utrie: 1.0.2
dev: false
/css-loader@6.7.1: /css-loader@6.7.1:
resolution: {integrity: sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==} resolution: {integrity: sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==}
engines: {node: '>= 12.13.0'} engines: {node: '>= 12.13.0'}
@ -6961,6 +7283,10 @@ packages:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
dev: true dev: true
/csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
dev: false
/current-script-polyfill@1.0.0: /current-script-polyfill@1.0.0:
resolution: {integrity: sha512-qv8s+G47V6Hq+g2kRE5th+ASzzrL7b6l+tap1DHKK25ZQJv3yIFhH96XaQ7NGL+zRW3t/RDbweJf/dJDe5Z5KA==} resolution: {integrity: sha512-qv8s+G47V6Hq+g2kRE5th+ASzzrL7b6l+tap1DHKK25ZQJv3yIFhH96XaQ7NGL+zRW3t/RDbweJf/dJDe5Z5KA==}
dev: true dev: true
@ -6969,10 +7295,175 @@ packages:
resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==}
dev: true dev: true
/d3-array@3.2.4:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'}
dependencies:
internmap: 2.0.3
dev: false
/d3-binarytree@1.0.2:
resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==}
dev: false
/d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
dev: false
/d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
dev: false
/d3-dsv@3.0.1:
resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
engines: {node: '>=12'}
hasBin: true
dependencies:
commander: 7.2.0
iconv-lite: 0.6.3
rw: 1.3.3
dev: false
/d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
dev: false
/d3-fetch@3.0.1:
resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
engines: {node: '>=12'}
dependencies:
d3-dsv: 3.0.1
dev: false
/d3-force-3d@3.0.6:
resolution: {integrity: sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==}
engines: {node: '>=12'}
dependencies:
d3-binarytree: 1.0.2
d3-dispatch: 3.0.1
d3-octree: 1.1.0
d3-quadtree: 3.0.1
d3-timer: 3.0.1
dev: false
/d3-force@3.0.0:
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
engines: {node: '>=12'}
dependencies:
d3-dispatch: 3.0.1
d3-quadtree: 3.0.1
d3-timer: 3.0.1
dev: false
/d3-format@3.1.2:
resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
engines: {node: '>=12'}
dev: false
/d3-geo-projection@4.0.0:
resolution: {integrity: sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==}
engines: {node: '>=12'}
hasBin: true
dependencies:
commander: 7.2.0
d3-array: 3.2.4
d3-geo: 3.1.1
dev: false
/d3-geo@3.1.1:
resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
dev: false
/d3-hierarchy@3.1.2:
resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
engines: {node: '>=12'}
dev: false
/d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
dependencies:
d3-color: 3.1.0
dev: false
/d3-octree@1.1.0:
resolution: {integrity: sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==}
dev: false
/d3-path@3.1.0:
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
engines: {node: '>=12'}
dev: false
/d3-polygon@1.0.6: /d3-polygon@1.0.6:
resolution: {integrity: sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==} resolution: {integrity: sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==}
dev: true dev: true
/d3-quadtree@3.0.1:
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
engines: {node: '>=12'}
dev: false
/d3-random@3.0.1:
resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
engines: {node: '>=12'}
dev: false
/d3-regression@1.3.10:
resolution: {integrity: sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==}
dev: false
/d3-scale-chromatic@3.1.0:
resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
engines: {node: '>=12'}
dependencies:
d3-color: 3.1.0
d3-interpolate: 3.0.1
dev: false
/d3-scale@4.0.2:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
d3-format: 3.1.2
d3-interpolate: 3.0.1
d3-time: 3.1.0
d3-time-format: 4.1.0
dev: false
/d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'}
dependencies:
d3-path: 3.1.0
dev: false
/d3-time-format@4.1.0:
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
engines: {node: '>=12'}
dependencies:
d3-time: 3.1.0
dev: false
/d3-time@3.1.0:
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
dev: false
/d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
dev: false
/d@1.0.2: /d@1.0.2:
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
@ -7998,7 +8489,6 @@ packages:
/eventemitter3@5.0.1: /eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
dev: true
/events-okam@3.3.0: /events-okam@3.3.0:
resolution: {integrity: sha512-6iR7z9hAJEwrT+D2Ywg6Fx62HSmN86OlcvPdrnq1JBeFr30dMF6l+j7M3VabjHfIi2KMtF8rO0J1rIZEfwMAwg==} resolution: {integrity: sha512-6iR7z9hAJEwrT+D2Ywg6Fx62HSmN86OlcvPdrnq1JBeFr30dMF6l+j7M3VabjHfIi2KMtF8rO0J1rIZEfwMAwg==}
@ -8153,7 +8643,6 @@ packages:
/fast-deep-equal@3.1.3: /fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
/fast-glob@3.2.12: /fast-glob@3.2.12:
resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
@ -8309,6 +8798,11 @@ packages:
deprecated: flatten is deprecated in favor of utility frameworks such as lodash. deprecated: flatten is deprecated in favor of utility frameworks such as lodash.
dev: true dev: true
/flru@1.0.2:
resolution: {integrity: sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==}
engines: {node: '>=6'}
dev: false
/flubber@0.4.2: /flubber@0.4.2:
resolution: {integrity: sha512-79RkJe3rA4nvRCVc2uXjj7U/BAUq84TS3KHn6c0Hr9K64vhj83ZNLUziNx4pJoBumSPhOl5VjH+Z0uhi+eE8Uw==} resolution: {integrity: sha512-79RkJe3rA4nvRCVc2uXjj7U/BAUq84TS3KHn6c0Hr9K64vhj83ZNLUziNx4pJoBumSPhOl5VjH+Z0uhi+eE8Uw==}
dependencies: dependencies:
@ -8556,6 +9050,10 @@ packages:
resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==} resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==}
dev: true dev: true
/gl-matrix@3.4.4:
resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==}
dev: false
/glob-parent@5.1.2: /glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -8851,6 +9349,14 @@ packages:
tapable: 2.2.1 tapable: 2.2.1
dev: true dev: true
/html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
dev: false
/htmlparser2@6.1.0: /htmlparser2@6.1.0:
resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==}
dependencies: dependencies:
@ -8862,6 +9368,7 @@ packages:
/http-cache-semantics@4.2.0: /http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
requiresBuild: true
dev: false dev: false
optional: true optional: true
@ -9074,6 +9581,11 @@ packages:
side-channel: 1.0.6 side-channel: 1.0.6
dev: true dev: true
/internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
dev: false
/intersection-observer@0.12.2: /intersection-observer@0.12.2:
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
dev: true dev: true
@ -9109,6 +9621,7 @@ packages:
/ip-address@10.1.0: /ip-address@10.1.0:
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
requiresBuild: true
dev: false dev: false
optional: true optional: true
@ -9590,7 +10103,6 @@ packages:
/js-tokens@4.0.0: /js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: true
/js-yaml@3.14.1: /js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
@ -10045,7 +10557,6 @@ packages:
hasBin: true hasBin: true
dependencies: dependencies:
js-tokens: 4.0.0 js-tokens: 4.0.0
dev: true
/lower-case@2.0.2: /lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
@ -11107,6 +11618,10 @@ packages:
sha.js: 2.4.11 sha.js: 2.4.11
dev: true dev: true
/pdfast@0.2.0:
resolution: {integrity: sha512-cq6TTu6qKSFUHwEahi68k/kqN2mfepjkGrG9Un70cgdRRKLKY6Rf8P8uvP2NvZktaQZNF3YE7agEkLj0vGK9bA==}
dev: false
/performance-now@2.1.0: /performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
dev: true dev: true
@ -12918,7 +13433,6 @@ packages:
loose-envify: 1.4.0 loose-envify: 1.4.0
react: 18.3.1 react: 18.3.1
scheduler: 0.23.2 scheduler: 0.23.2
dev: true
/react-easy-crop@5.2.0(react-dom@18.3.1)(react@18.3.1): /react-easy-crop@5.2.0(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-gjb7jN+WnwfgpbNUI2jSwyoIxF1sJ0PVSNVgEysAgF1rj8AqR75fqmdvqZ6PFVgEX3rT1G4HJELesiQXr2ZvAg==} resolution: {integrity: sha512-gjb7jN+WnwfgpbNUI2jSwyoIxF1sJ0PVSNVgEysAgF1rj8AqR75fqmdvqZ6PFVgEX3rT1G4HJELesiQXr2ZvAg==}
@ -13189,7 +13703,6 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
dev: true
/reactcss@1.2.3(react@18.3.1): /reactcss@1.2.3(react@18.3.1):
resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==} resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==}
@ -13485,6 +13998,10 @@ packages:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
dev: true dev: true
/rw@1.3.3:
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
dev: false
/rxjs@7.8.1: /rxjs@7.8.1:
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
dependencies: dependencies:
@ -13534,7 +14051,6 @@ packages:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
dev: true
/schema-utils@3.3.0: /schema-utils@3.3.0:
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
@ -13853,6 +14369,7 @@ packages:
/socks@2.8.7: /socks@2.8.7:
resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
requiresBuild: true
dependencies: dependencies:
ip-address: 10.1.0 ip-address: 10.1.0
smart-buffer: 4.2.0 smart-buffer: 4.2.0
@ -14349,6 +14866,10 @@ packages:
resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}
dev: true dev: true
/svg-path-parser@1.1.0:
resolution: {integrity: sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==}
dev: false
/svg-path-properties@0.2.2: /svg-path-properties@0.2.2:
resolution: {integrity: sha512-GmrB+b6woz6CCdQe6w1GHs/1lt25l7SR5hmhF8jRdarpv/OgjLyuQygLu1makJapixeb1aQhP/Oa1iKi93o/aQ==} resolution: {integrity: sha512-GmrB+b6woz6CCdQe6w1GHs/1lt25l7SR5hmhF8jRdarpv/OgjLyuQygLu1makJapixeb1aQhP/Oa1iKi93o/aQ==}
dev: true dev: true
@ -14468,6 +14989,12 @@ packages:
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
dev: false dev: false
/text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
dependencies:
utrie: 1.0.2
dev: false
/text-table@0.2.0: /text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
dev: true dev: true
@ -14647,7 +15174,6 @@ packages:
/tslib@2.8.1: /tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
dev: true
/tsutils@3.21.0(typescript@5.2.2): /tsutils@3.21.0(typescript@5.2.2):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
@ -15084,6 +15610,12 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4.0'}
/utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
dependencies:
base64-arraybuffer: 1.0.2
dev: false
/uuid@11.0.3: /uuid@11.0.3:
resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==}
hasBin: true hasBin: true
@ -15509,7 +16041,3 @@ packages:
- encoding - encoding
- supports-color - supports-color
dev: false dev: false
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

View File

@ -220,4 +220,18 @@ update_auth_config() {
fi fi
} }
record_cron_stat() {
local ref_id="$1"
local exit_code="${2:-0}"
local elapsed="${3:-0}"
[[ $ref_id ]] && [[ $ref_id -gt 0 ]] 2>/dev/null || return
curl -s --noproxy "*" "http://0.0.0.0:${ql_port:-5700}/open/dashboard/record" \
-X POST \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
--data-raw "{\"ref_id\":$ref_id,\"code\":$exit_code,\"elapsed\":$elapsed}" \
--compressed
}
get_token get_token

View File

@ -294,6 +294,7 @@ fi
set_u_on="false" set_u_on="false"
check_nounset check_nounset
main "${task_shell_params[@]}" main "${task_shell_params[@]}"
_task_exit_code=$?
if [[ "$set_u_on" == 'true' ]]; then if [[ "$set_u_on" == 'true' ]]; then
set -u set -u
fi fi
@ -305,4 +306,4 @@ if [[ $isJsOrPythonFile == 'true' ]]; then
fi fi
run_task_after "${task_shell_params[@]}" run_task_after "${task_shell_params[@]}"
clear_env clear_env
handle_task_end "${task_shell_params[@]}" handle_task_end "${task_shell_params[@]}" "$_task_exit_code"

View File

@ -388,18 +388,25 @@ handle_task_end() {
local end_time=$(format_time "$time_format" "$etime") local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime") local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp)) local diff_time=$(($end_timestamp - $begin_timestamp))
local suffix="" local exit_code="${@: -1}"
[[ "${MANUAL:=}" == "true" ]] && suffix="(手动停止)"
[[ "$diff_time" == 0 ]] && diff_time=1 [[ "$diff_time" == 0 ]] && diff_time=1
if [[ $ID ]]; then if [[ $ID ]]; then
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time") local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time")
if [[ $error ]]; then if [[ $error ]]; then
error_message=", 任务状态更新失败(${error})" error_message=", 状态更新失败(${error})"
fi fi
fi fi
echo -e "\n## 执行结束$suffix... $end_time 耗时 $diff_time${error_message:=}     "
record_cron_stat "$ID" "${exit_code:-0}" "$diff_time"
if [[ "${MANUAL:=}" == "true" ]]; then
echo -e "\n## 已停止 ⏹... $end_time 耗时 $diff_time${error_message:=}     "
elif [[ $exit_code -eq 0 ]]; then
echo -e "\n## 完成 ✅... $end_time 耗时 $diff_time${error_message:=}     "
else
echo -e "\n## 失败 ❌ (退出码 ${exit_code})... $end_time 耗时 $diff_time${error_message:=}     "
fi
} }
init_env init_env

View File

@ -1,5 +1,5 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { SettingOutlined } from '@ant-design/icons'; import { SettingOutlined, DashboardOutlined } from '@ant-design/icons';
import IconFont from '@/components/iconfont'; import IconFont from '@/components/iconfont';
import { BasicLayoutProps } from '@ant-design/pro-layout'; import { BasicLayoutProps } from '@ant-design/pro-layout';
@ -24,6 +24,12 @@ export default {
hideInMenu: true, hideInMenu: true,
component: '@/pages/error/index', component: '@/pages/error/index',
}, },
{
path: '/dashboard',
name: intl.get('统计面板'),
icon: <DashboardOutlined />,
component: '@/pages/dashboard/index',
},
{ {
path: '/crontab', path: '/crontab',
name: intl.get('定时任务'), name: intl.get('定时任务'),

View File

@ -212,6 +212,7 @@ body {
&.crontab-wrapper, &.crontab-wrapper,
&.log-wrapper, &.log-wrapper,
&.env-wrapper, &.env-wrapper,
&.dashboard-wrapper,
&.config-wrapper { &.config-wrapper {
.CodeMirror { .CodeMirror {
width: calc(100vw - 24px); width: calc(100vw - 24px);

View File

@ -556,5 +556,34 @@
"批量": "Batch", "批量": "Batch",
"全局SSH私钥": "Global SSH Private Key", "全局SSH私钥": "Global SSH Private Key",
"用于访问所有私有仓库的全局SSH私钥": "Global SSH private key for accessing all private repositories", "用于访问所有私有仓库的全局SSH私钥": "Global SSH private key for accessing all private repositories",
"请输入完整的SSH私钥内容": "Please enter the complete SSH private key content" "请输入完整的SSH私钥内容": "Please enter the complete SSH private key content",
"统计面板": "Dashboard",
"总任务": "Total Tasks",
"今日执行": "Today Runs",
"今日成功": "Success",
"今日失败": "Failed",
"成功率": "Success Rate",
"平均耗时": "Avg Time",
"近 7 日趋势": "7-Day Trend",
"今日耗时 Top 5": "Top 5 by Time",
"今日执行次数 Top 5": "Top 5 by Runs",
"实时运行态": "Runtime",
"系统资源": "System",
"排队中": "Queued",
"暂无运行中任务": "No running tasks",
"暂无数据": "No data",
"总执行": "Total",
"次数": "Runs",
"最长单次": "Max Time",
"已运行": "Elapsed",
"系统运行": "System Uptime",
"内存使用": "Memory",
"堆内存": "Heap",
"CPU 核心": "CPU Cores",
"负载 1m": "Load 1m",
"平台": "Platform",
"24小时未运行": "Idle 24h+",
"标签统计": "Label Stats",
"任务数": "Tasks",
"内存": "Memory"
} }

View File

@ -556,5 +556,34 @@
"批量": "批量", "批量": "批量",
"全局SSH私钥": "全局SSH私钥", "全局SSH私钥": "全局SSH私钥",
"用于访问所有私有仓库的全局SSH私钥": "用于访问所有私有仓库的全局SSH私钥", "用于访问所有私有仓库的全局SSH私钥": "用于访问所有私有仓库的全局SSH私钥",
"请输入完整的SSH私钥内容": "请输入完整的SSH私钥内容" "请输入完整的SSH私钥内容": "请输入完整的SSH私钥内容",
"统计面板": "统计面板",
"总任务": "总任务",
"今日执行": "今日执行",
"今日成功": "今日成功",
"今日失败": "今日失败",
"成功率": "成功率",
"平均耗时": "平均耗时",
"近 7 日趋势": "近 7 日趋势",
"今日耗时 Top 5": "今日耗时 Top 5",
"今日执行次数 Top 5": "今日执行次数 Top 5",
"实时运行态": "实时运行态",
"系统资源": "系统资源",
"排队中": "排队中",
"暂无运行中任务": "暂无运行中任务",
"暂无数据": "暂无数据",
"总执行": "总执行",
"次数": "次数",
"最长单次": "最长单次",
"已运行": "已运行",
"系统运行": "系统运行",
"内存使用": "内存使用",
"堆内存": "堆内存",
"CPU 核心": "CPU 核心",
"负载 1m": "负载 1m",
"平台": "平台",
"24小时未运行": "24小时未运行",
"标签统计": "标签统计",
"任务数": "任务数",
"内存": "内存"
} }

View File

@ -0,0 +1,359 @@
import { useEffect, useState, useCallback } from 'react';
import { Card, Col, Row, Statistic, Table, Tag, Spin, Empty } from 'antd';
import {
CheckCircleOutlined,
CloseCircleOutlined,
ClockCircleOutlined,
ThunderboltOutlined,
StopOutlined,
BarChartOutlined,
} from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-layout';
import { useOutletContext } from '@umijs/max';
import { Area, Gauge } from '@ant-design/plots';
import intl from 'react-intl-universal';
import { SharedContext } from '@/layouts';
import { request } from '@/utils/http';
import CronLogModal from '../crontab/logModal';
interface Overview {
total: number;
enabled: number;
disabled: number;
todayRuns: number;
todaySuccess: number;
todayFail: number;
successRate: string;
avgTime: number;
}
interface TrendItem {
date: string;
total: number;
success: number;
fail: number;
}
interface TopItem {
rank: number;
name: string;
avgTime?: number;
maxTime?: number;
runCount?: number;
successRate?: string;
}
interface Runtime {
runningCount: number;
queuedCount: number;
running: Array<{ id: number; name: string; pid: number; elapsed: number; logPath: string }>;
idleTasks: Array<{ id: number; name: string; lastRun: string }>;
}
interface SystemInfo {
platform: string;
uptime: number;
memTotal: number;
memFree: number;
memUsagePercent: string;
heapUsed: number;
heapTotal: number;
loadAvg: number[];
cpus: number;
}
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
};
const formatSeconds = (s: number) => {
if (s < 60) return `${s}s`;
if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
return `${h}h ${m}m`;
};
const REFRESH_INTERVAL = 30000;
const Dashboard = () => {
const { headerStyle, theme } = useOutletContext<SharedContext>();
const isDark = theme === 'vs-dark';
const [overview, setOverview] = useState<Overview | null>(null);
const [trend, setTrend] = useState<TrendItem[]>([]);
const [topTime, setTopTime] = useState<TopItem[]>([]);
const [topCount, setTopCount] = useState<TopItem[]>([]);
const [runtime, setRuntime] = useState<Runtime | null>(null);
const [system, setSystem] = useState<SystemInfo | null>(null);
const [labels, setLabels] = useState<any[]>([]);
const [logCron, setLogCron] = useState<any>(null);
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
try {
const [overviewRes, trendRes, topTimeRes, topCountRes, runtimeRes, systemRes, labelsRes] =
await Promise.allSettled([
request.get('/api/dashboard/overview'),
request.get('/api/dashboard/trend'),
request.get('/api/dashboard/top-time'),
request.get('/api/dashboard/top-count'),
request.get('/api/dashboard/runtime'),
request.get('/api/dashboard/system'),
request.get('/api/dashboard/labels'),
]);
if (overviewRes.status === 'fulfilled' && overviewRes.value.code === 200)
setOverview(overviewRes.value.data);
if (trendRes.status === 'fulfilled' && trendRes.value.code === 200)
setTrend(trendRes.value.data);
if (topTimeRes.status === 'fulfilled' && topTimeRes.value.code === 200)
setTopTime(topTimeRes.value.data);
if (topCountRes.status === 'fulfilled' && topCountRes.value.code === 200)
setTopCount(topCountRes.value.data);
if (runtimeRes.status === 'fulfilled' && runtimeRes.value.code === 200)
setRuntime(runtimeRes.value.data);
if (systemRes.status === 'fulfilled' && systemRes.value.code === 200)
setSystem(systemRes.value.data);
if (labelsRes.status === 'fulfilled' && labelsRes.value.code === 200)
setLabels(labelsRes.value.data);
} catch (e) {
console.error('[dashboard] fetch error', e);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
const poll = async () => {
await fetchData();
timer = setTimeout(poll, REFRESH_INTERVAL);
};
fetchData();
timer = setTimeout(poll, REFRESH_INTERVAL);
return () => clearTimeout(timer);
}, [fetchData]);
const trendConfig = {
data: trend.flatMap((d) => [
{ date: d.date, value: d.total, type: intl.get('总执行') },
{ date: d.date, value: d.success, type: intl.get('成功') },
{ date: d.date, value: d.fail, type: intl.get('失败') },
]),
xField: 'date',
yField: 'value',
seriesField: 'type',
smooth: true,
height: 260,
color: ['#1677ff', '#52c41a', '#ff4d4f'],
legend: { position: 'top' as const },
theme: isDark ? 'dark' : 'light',
xAxis: {
label: { style: { fill: isDark ? '#aaa' : '#333' } },
},
yAxis: {
label: { style: { fill: isDark ? '#aaa' : '#333' } },
grid: { line: { style: { stroke: isDark ? '#333' : '#eee' } } },
},
};
const runtimePagination = {
pageSize: 5,
showSizeChanger: false,
showTotal: (total: number) => `${total}`,
};
if (loading) return <Spin size="large" style={{ display: 'block', marginTop: 100 }} />;
return (
<PageContainer
title={intl.get('统计面板')}
header={{ style: headerStyle }}
className={'ql-container-wrapper dashboard-wrapper'}
>
<Row gutter={[16, 16]}>
<Col xs={12} sm={8} md={6} lg={3}>
<Card size="small"><Statistic title={intl.get('总任务')} value={overview?.total || 0} prefix={<BarChartOutlined />} /></Card>
</Col>
<Col xs={12} sm={8} md={6} lg={3}>
<Card size="small"><Statistic title={intl.get('已启用')} value={overview?.enabled || 0} valueStyle={{ color: '#1677ff' }} prefix={<CheckCircleOutlined />} /></Card>
</Col>
<Col xs={12} sm={8} md={6} lg={3}>
<Card size="small"><Statistic title={intl.get('今日执行')} value={overview?.todayRuns || 0} valueStyle={{ color: '#1677ff' }} prefix={<ThunderboltOutlined />} /></Card>
</Col>
<Col xs={12} sm={8} md={6} lg={3}>
<Card size="small"><Statistic title={intl.get('成功率')} value={`${overview?.successRate || '0'}%`} valueStyle={{ color: '#52c41a' }} /></Card>
</Col>
<Col xs={12} sm={8} md={6} lg={3}>
<Card size="small"><Statistic title={intl.get('今日成功')} value={overview?.todaySuccess || 0} valueStyle={{ color: '#52c41a' }} prefix={<CheckCircleOutlined />} /></Card>
</Col>
<Col xs={12} sm={8} md={6} lg={3}>
<Card size="small"><Statistic title={intl.get('今日失败')} value={overview?.todayFail || 0} valueStyle={{ color: '#ff4d4f' }} prefix={<CloseCircleOutlined />} /></Card>
</Col>
<Col xs={12} sm={8} md={6} lg={3}>
<Card size="small"><Statistic title={intl.get('平均耗时')} value={overview?.avgTime ? `${(overview.avgTime / 1000).toFixed(1)}s` : '-'} prefix={<ClockCircleOutlined />} /></Card>
</Col>
<Col xs={12} sm={8} md={6} lg={3}>
<Card size="small"><Statistic title={intl.get('已禁用')} value={overview?.disabled || 0} prefix={<StopOutlined />} /></Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={24}>
<Card title={intl.get('近 7 日趋势')} size="small">
{trend.length > 0 ? <Area {...trendConfig} /> : <Empty description={intl.get('暂无数据')} />}
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col xs={24} lg={12}>
<Card title={intl.get('今日耗时 Top 5')} size="small">
<Table
dataSource={topTime}
rowKey="rank"
pagination={false}
size="small"
columns={[
{ title: '#', dataIndex: 'rank', width: 40 },
{ title: intl.get('定时任务'), dataIndex: 'name', ellipsis: true },
{ title: intl.get('平均耗时'), dataIndex: 'avgTime', width: 100, render: (v: number) => v ? `${(v / 1000).toFixed(1)}s` : '-' },
{ title: intl.get('最长单次'), dataIndex: 'maxTime', width: 100, render: (v: number) => v ? `${(v / 1000).toFixed(1)}s` : '-' },
]}
/>
</Card>
</Col>
<Col xs={24} lg={12}>
<Card title={intl.get('今日执行次数 Top 5')} size="small">
<Table
dataSource={topCount}
rowKey="rank"
pagination={false}
size="small"
columns={[
{ title: '#', dataIndex: 'rank', width: 40 },
{ title: intl.get('定时任务'), dataIndex: 'name', ellipsis: true },
{ title: intl.get('次数'), dataIndex: 'runCount', width: 60 },
{ title: intl.get('平均耗时'), dataIndex: 'avgTime', width: 100, render: (v: number) => v ? `${(v / 1000).toFixed(1)}s` : '-' },
{ title: intl.get('成功率'), dataIndex: 'successRate', width: 80, render: (v: string) => `${v}%` },
]}
/>
</Card>
</Col>
</Row>
{labels.length > 0 && (
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={24}>
<Card title={intl.get('标签统计')} size="small">
<Table
dataSource={labels}
rowKey="label"
pagination={false}
size="small"
columns={[
{ title: intl.get('标签'), dataIndex: 'label', width: 150, render: (v: string) => <Tag>{v}</Tag> },
{ title: intl.get('任务数'), dataIndex: 'count', width: 80 },
{ title: intl.get('今日执行'), dataIndex: 'todayRuns', width: 100 },
{ title: intl.get('成功率'), dataIndex: 'successRate', width: 100, render: (v: string) => `${v}%` },
{ title: intl.get('平均耗时'), dataIndex: 'avgTime', width: 120, render: (v: number) => v ? `${(v / 1000).toFixed(1)}s` : '-' },
]}
/>
</Card>
</Col>
</Row>
)}
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col xs={24} lg={12}>
<Card
title={intl.get('实时运行态')}
size="small"
extra={
<>
<Tag color="processing">{intl.get('运行中')} {runtime?.runningCount || 0}</Tag>
<Tag color="warning">{intl.get('排队中')} {runtime?.queuedCount || 0}</Tag>
</>
}
>
<Table
dataSource={runtime?.running || []}
rowKey="id"
pagination={runtime && runtime.running.length > 5 ? runtimePagination : false}
size="small"
locale={{ emptyText: <Empty description={intl.get('暂无运行中任务')} /> }}
columns={[
{ title: intl.get('定时任务'), dataIndex: 'name', ellipsis: true },
{ title: 'PID', dataIndex: 'pid', width: 80 },
{ title: intl.get('已运行'), dataIndex: 'elapsed', width: 100, render: (v: number) => v ? formatSeconds(v) : '-' },
{ title: intl.get('日志'), dataIndex: 'id', width: 60, render: (id, record) => <a onClick={() => { localStorage.setItem('logCron', String(id)); setLogCron({ id, name: record.name }); }}>{intl.get('查看')}</a> },
]}
/>
{runtime?.idleTasks && runtime.idleTasks.length > 0 && (
<>
<div style={{ marginTop: 12, fontSize: 13, color: '#ff7a00' }}>
{intl.get('24小时未运行')} ({runtime.idleTasks.length})
</div>
<Table
dataSource={runtime.idleTasks}
rowKey="id"
pagination={false}
size="small"
showHeader={false}
columns={[
{ dataIndex: 'name', ellipsis: true },
{ dataIndex: 'lastRun', width: 110, render: (v: string) => <span style={{ color: '#999' }}>{v}</span> },
]}
/>
</>
)}
</Card>
</Col>
<Col xs={24} lg={12}>
<Card title={intl.get('系统资源')} size="small">
{system && (
<Row gutter={[16, 16]} align="middle">
<Col span={12}>
<Gauge
height={300}
data={{
target: Number(system.memUsagePercent),
total: 100,
name: '内存使用率',
}}
style={{
textY: '70%',
textContent: (target: number, total: number) => {
return `${intl.get('内存')}${target}%`;
},
}}
/>
</Col>
<Col span={12}>
<Statistic title={intl.get('系统运行')} value={formatSeconds(system.uptime)} />
<Statistic title={intl.get('堆内存')} value={`${system.heapUsed} MB`} valueStyle={{ fontSize: 16 }} />
<div style={{ marginTop: 8, color: '#888', fontSize: 12 }}>
{intl.get('负载 1m')}: {system.loadAvg?.[0] || '-'} | CPU: {system.cpus} {intl.get('核心')} | {system.platform}
</div>
</Col>
</Row>
)}
</Card>
</Col>
</Row>
{logCron && (
<CronLogModal
cron={logCron}
handleCancel={() => setLogCron(null)}
/>
)}
</PageContainer>
);
};
export default Dashboard;

View File

@ -109,7 +109,7 @@ const Login = () => {
), ),
}); });
reloadUser(true); reloadUser(true);
history.push('/crontab'); history.push('/dashboard');
} else if (code === 410) { } else if (code === 410) {
setWaitTime(data); setWaitTime(data);
} else if (code === 420) { } else if (code === 420) {
@ -136,7 +136,7 @@ const Login = () => {
useEffect(() => { useEffect(() => {
const isAuth = localStorage.getItem(config.authKey); const isAuth = localStorage.getItem(config.authKey);
if (isAuth) { if (isAuth) {
history.push('/crontab'); history.push('/dashboard');
} }
}, []); }, []);