diff --git a/back/api/dashboard.ts b/back/api/dashboard.ts new file mode 100644 index 00000000..2e23daa5 --- /dev/null +++ b/back/api/dashboard.ts @@ -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 = {}; + 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 = {}; + 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 = {}; + 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 = {}; + stats.forEach((s: any) => { statMap[s.ref_id] = s; }); + + const labelMap: Record = {}; + 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); + } + }, + ); +}; diff --git a/back/api/index.ts b/back/api/index.ts index 6dcab62f..443c8e5c 100644 --- a/back/api/index.ts +++ b/back/api/index.ts @@ -10,6 +10,7 @@ import dependence from './dependence'; import system from './system'; import subscription from './subscription'; import update from './update'; +import dashboard from './dashboard'; import health from './health'; export default () => { @@ -25,6 +26,7 @@ export default () => { system(app); subscription(app); update(app); + dashboard(app); health(app); return app; diff --git a/back/data/cronStats.ts b/back/data/cronStats.ts new file mode 100644 index 00000000..660b7c8d --- /dev/null +++ b/back/data/cronStats.ts @@ -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 {} + +export const CrontabStatModel = sequelize.define( + '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'] }, + ], + }, +); diff --git a/back/data/open.ts b/back/data/open.ts index 2c0ba9aa..f508c70b 100644 --- a/back/data/open.ts +++ b/back/data/open.ts @@ -24,7 +24,7 @@ export interface AppToken { 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 {} export const AppModel = sequelize.define('App', { diff --git a/back/loaders/db.ts b/back/loaders/db.ts index 987c6cbe..f2990611 100644 --- a/back/loaders/db.ts +++ b/back/loaders/db.ts @@ -6,6 +6,7 @@ import { AppModel } from '../data/open'; import { SystemModel } from '../data/system'; import { SubscriptionModel } from '../data/subscription'; import { CrontabViewModel } from '../data/cronView'; +import { CrontabStatModel } from '../data/cronStats'; import { sequelize } from '../data'; export default async () => { @@ -17,6 +18,7 @@ export default async () => { await EnvModel.sync(); await SubscriptionModel.sync(); await CrontabViewModel.sync(); + await CrontabStatModel.sync(); // 初始化新增字段 const migrations = [ diff --git a/back/loaders/initData.ts b/back/loaders/initData.ts index 37bbdd6b..264d7e05 100644 --- a/back/loaders/initData.ts +++ b/back/loaders/initData.ts @@ -36,10 +36,15 @@ export default async () => { if (!systemApp) { systemApp = await AppModel.create({ name: 'system', - scopes: ['crons', 'system'], + scopes: ['crons', 'system', 'dashboard'], client_id: createRandomString(12, 12), 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({ where: { type: AuthDataType.systemConfig }, diff --git a/back/services/cron.ts b/back/services/cron.ts index def418a4..9bc34a09 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -573,7 +573,6 @@ export default class CronService { JSON.stringify(params), code, ); - // Close the stream after task completion await logStreamManager.closeStream(absolutePath); await CrontabModel.update( { status: CrontabStatus.idle, pid: undefined }, diff --git a/package.json b/package.json index 3e942705..cda82f41 100644 --- a/package.json +++ b/package.json @@ -73,12 +73,16 @@ } }, "dependencies": { + "@ant-design/plots": "^2.6.8", + "@bufbuild/protobuf": "^2.10.0", "@grpc/grpc-js": "^1.14.0", "@grpc/proto-loader": "^0.8.0", + "@keyv/sqlite": "^4.0.1", "@otplib/preset-default": "^12.0.1", "body-parser": "^1.20.3", "celebrate": "^15.0.3", "chokidar": "^4.0.1", + "compression": "^1.7.4", "cors": "^2.8.5", "cron-parser": "^5.4.0", "cross-spawn": "^7.0.6", @@ -88,51 +92,48 @@ "express-jwt": "^8.4.1", "express-rate-limit": "^7.4.1", "express-urlrewrite": "^2.0.3", - "undici": "^7.9.0", + "helmet": "^8.1.0", "hpagent": "^1.2.0", "http-proxy-middleware": "^3.0.3", "iconv-lite": "^0.6.3", + "ip2region": "2.3.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", + "keyv": "^5.2.3", "lodash": "^4.17.21", "multer": "2.1.1", "node-schedule": "^2.1.0", "nodemailer": "^8.0.1", "p-queue-cjs": "7.3.4", - "@bufbuild/protobuf": "^2.10.0", + "proper-lockfile": "^4.1.2", "ps-tree": "^1.2.0", "reflect-metadata": "^0.2.2", + "request-ip": "3.3.0", "sequelize": "^6.37.5", "sockjs": "^0.3.24", "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3", "toad-scheduler": "^3.0.1", "typedi": "^0.10.0", + "undici": "^7.9.0", "uuid": "^11.0.3", "winston": "^3.17.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" + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { - "moment": "2.30.1", "@ant-design/icons": "^5.0.1", "@ant-design/pro-layout": "6.38.22", - "@codemirror/view": "6.39.16", "@codemirror/state": "6.5.4", + "@codemirror/view": "6.39.16", "@monaco-editor/react": "4.2.1", "@react-hook/resize-observer": "^2.0.2", - "react-router-dom": "6.26.1", "@types/body-parser": "^1.19.2", + "@types/compression": "^1.7.2", "@types/cors": "^2.8.12", "@types/cross-spawn": "^6.0.2", "@types/express": "^4.17.13", "@types/express-jwt": "^6.0.4", "@types/file-saver": "2.0.2", + "@types/helmet": "^4.0.0", "@types/js-yaml": "^4.0.5", "@types/jsonwebtoken": "^8.5.8", "@types/lodash": "^4.14.185", @@ -140,17 +141,17 @@ "@types/node": "^17.0.21", "@types/node-schedule": "^1.3.2", "@types/nodemailer": "^6.4.4", + "@types/proper-lockfile": "^4.1.4", + "@types/ps-tree": "^1.1.6", "@types/qrcode.react": "^1.0.2", "@types/react": "^18.0.20", "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "^18.0.6", + "@types/request-ip": "0.0.41", "@types/serve-handler": "^6.1.1", "@types/sockjs": "^0.3.33", "@types/sockjs-client": "^1.5.1", "@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/react-codemirror": "^4.21.9", "@umijs/max": "^4.4.4", @@ -162,9 +163,9 @@ "axios": "^1.4.0", "compression-webpack-plugin": "9.2.0", "concurrently": "^7.0.0", - "react-hotkeys-hook": "^4.6.1", "file-saver": "2.0.2", "lint-staged": "^13.0.3", + "moment": "2.30.1", "monaco-editor": "0.33.0", "nodemon": "^3.0.1", "prettier": "^2.5.1", @@ -180,7 +181,9 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.3.1", + "react-hotkeys-hook": "^4.6.1", "react-intl-universal": "^2.12.0", + "react-router-dom": "6.26.1", "react-split-pane": "^0.1.92", "sockjs-client": "^1.6.0", "ts-node": "^10.9.2", @@ -188,8 +191,6 @@ "tslib": "^2.4.0", "typescript": "5.2.2", "vh-check": "^2.0.5", - "virtualizedtableforantd4": "1.3.0", - "@types/compression": "^1.7.2", - "@types/helmet": "^4.0.0" + "virtualizedtableforantd4": "1.3.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b26696ac..f15057a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,11 +1,18 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + overrides: sqlite3: git+https://github.com/whyour/node-sqlite3.git#v1.0.3 '@codemirror/state': 6.5.4 '@codemirror/view': 6.39.16 dependencies: + '@ant-design/plots': + specifier: ^2.6.8 + version: 2.6.8(react-dom@18.3.1)(react@18.3.1) '@bufbuild/protobuf': specifier: ^2.10.0 version: 2.10.0 @@ -384,6 +391,22 @@ packages: resolution: {integrity: sha512-0vr5GCwM7xlAl6NxG1lPbABO+SYioNJL3HVy2FA8wTlsIMoZvQwcwsxTw6eLQCiN9V2UQ8kBtfz8DW8utVVE5w==} 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: resolution: {integrity: sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==} dependencies: @@ -469,6 +492,27 @@ packages: resolution: {integrity: sha512-MLm1FUpg02fP615ShQnCUN9la2E4RylDxKyolkGqAWTIHO4HyGM0A5x71AMALEyP/bC+UEEWBGSQ+D4/8hQ+ww==} 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): resolution: {integrity: sha512-mKOmNb7jc3Pz41RrPY7EFKRWBjLdN4tp9yzmRkS2g8K7P3pW435f7Ip6rc+58FWDzbZa8lElTGPxAoFB/dq7LA==} peerDependencies: @@ -903,6 +947,184 @@ packages: resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} 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: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -1351,7 +1573,6 @@ packages: /@babel/runtime@7.28.6: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} - dev: true /@babel/template@7.25.9: resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} @@ -1409,8 +1630,8 @@ packages: resolution: {integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==} peerDependencies: '@codemirror/language': ^6.0.0 - '@codemirror/state': ^6.0.0 - '@codemirror/view': ^6.0.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.10.6 @@ -3666,8 +3887,8 @@ packages: peerDependencies: '@codemirror/autocomplete': ^6.0.0 '@codemirror/language': ^6.0.0 - '@codemirror/state': ^6.0.0 - '@codemirror/view': ^6.0.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': ^1.0.0 '@lezer/highlight': ^1.0.0 '@lezer/lr': ^1.0.0 @@ -3686,8 +3907,8 @@ packages: peerDependencies: '@codemirror/autocomplete': ^6.0.0 '@codemirror/language': ^6.0.0 - '@codemirror/state': ^6.0.0 - '@codemirror/view': ^6.0.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': ^1.0.0 '@lezer/highlight': ^1.0.0 '@lezer/lr': ^1.0.0 @@ -3718,8 +3939,8 @@ packages: '@codemirror/lang-html': ^6.2.0 '@codemirror/lang-javascript': ^6.1.1 '@codemirror/language': ^6.0.0 - '@codemirror/state': ^6.0.0 - '@codemirror/view': ^6.0.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': ^1.0.0 '@lezer/highlight': ^1.0.0 '@lezer/javascript': ^1.2.0 @@ -4082,6 +4303,92 @@ packages: '@types/node': 17.0.45 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: resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} dependencies: @@ -4124,6 +4431,10 @@ packages: resolution: {integrity: sha512-xbqnZmGrCEqi/KUzOkeUSe77p7APvLuyellGaAoeww3CHJ1AbjQWjPSCFtKIzZn8L7LpEax4NXnC+gfa6nM7IA==} dev: true + /@types/geojson@7946.0.16: + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + dev: false + /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: @@ -4539,8 +4850,8 @@ packages: '@codemirror/language': '>=6.0.0' '@codemirror/lint': '>=6.0.0' '@codemirror/search': '>=6.0.0' - '@codemirror/state': '>=6.0.0' - '@codemirror/view': '>=6.0.0' + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 dependencies: '@codemirror/autocomplete': 6.20.1 '@codemirror/commands': 6.7.1 @@ -4599,9 +4910,9 @@ packages: resolution: {integrity: sha512-caYKGV6TfGLRV1HHD3p0G3FiVzKL1go7wes5XT2nWjB0+dTdyzyb81MKRSacptgZcotujfNO6QXn65uhETRAMw==} peerDependencies: '@babel/runtime': '>=7.11.0' - '@codemirror/state': '>=6.0.0' + '@codemirror/state': 6.5.4 '@codemirror/theme-one-dark': '>=6.0.0' - '@codemirror/view': '>=6.0.0' + '@codemirror/view': 6.39.16 codemirror: '>=6.0.0' react: '>=16.8.0 || 18' react-dom: '>=16.8.0 || 18' @@ -5390,6 +5701,7 @@ packages: /agentkeepalive@4.6.0: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} + requiresBuild: true dependencies: humanize-ms: 1.2.1 dev: false @@ -6018,6 +6330,11 @@ packages: resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} 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: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -6554,7 +6871,6 @@ packages: /commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} - dev: true /commander@8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} @@ -6873,6 +7189,12 @@ packages: postcss-selector-parser: 6.1.2 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: resolution: {integrity: sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==} engines: {node: '>= 12.13.0'} @@ -6961,6 +7283,10 @@ packages: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} dev: true + /csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dev: false + /current-script-polyfill@1.0.0: resolution: {integrity: sha512-qv8s+G47V6Hq+g2kRE5th+ASzzrL7b6l+tap1DHKK25ZQJv3yIFhH96XaQ7NGL+zRW3t/RDbweJf/dJDe5Z5KA==} dev: true @@ -6969,10 +7295,175 @@ packages: resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} 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: resolution: {integrity: sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==} 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: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} @@ -7998,7 +8489,6 @@ packages: /eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - dev: true /events-okam@3.3.0: resolution: {integrity: sha512-6iR7z9hAJEwrT+D2Ywg6Fx62HSmN86OlcvPdrnq1JBeFr30dMF6l+j7M3VabjHfIi2KMtF8rO0J1rIZEfwMAwg==} @@ -8153,7 +8643,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} @@ -8309,6 +8798,11 @@ packages: deprecated: flatten is deprecated in favor of utility frameworks such as lodash. dev: true + /flru@1.0.2: + resolution: {integrity: sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==} + engines: {node: '>=6'} + dev: false + /flubber@0.4.2: resolution: {integrity: sha512-79RkJe3rA4nvRCVc2uXjj7U/BAUq84TS3KHn6c0Hr9K64vhj83ZNLUziNx4pJoBumSPhOl5VjH+Z0uhi+eE8Uw==} dependencies: @@ -8556,6 +9050,10 @@ packages: resolution: {integrity: sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==} dev: true + /gl-matrix@3.4.4: + resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -8851,6 +9349,14 @@ packages: tapable: 2.2.1 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: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} dependencies: @@ -8862,6 +9368,7 @@ packages: /http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + requiresBuild: true dev: false optional: true @@ -9074,6 +9581,11 @@ packages: side-channel: 1.0.6 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: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} dev: true @@ -9109,6 +9621,7 @@ packages: /ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} + requiresBuild: true dev: false optional: true @@ -9590,7 +10103,6 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} @@ -10045,7 +10557,6 @@ packages: hasBin: true dependencies: js-tokens: 4.0.0 - dev: true /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -11107,6 +11618,10 @@ packages: sha.js: 2.4.11 dev: true + /pdfast@0.2.0: + resolution: {integrity: sha512-cq6TTu6qKSFUHwEahi68k/kqN2mfepjkGrG9Un70cgdRRKLKY6Rf8P8uvP2NvZktaQZNF3YE7agEkLj0vGK9bA==} + dev: false + /performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} dev: true @@ -12918,7 +13433,6 @@ packages: loose-envify: 1.4.0 react: 18.3.1 scheduler: 0.23.2 - dev: true /react-easy-crop@5.2.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-gjb7jN+WnwfgpbNUI2jSwyoIxF1sJ0PVSNVgEysAgF1rj8AqR75fqmdvqZ6PFVgEX3rT1G4HJELesiQXr2ZvAg==} @@ -13189,7 +13703,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: true /reactcss@1.2.3(react@18.3.1): resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==} @@ -13485,6 +13998,10 @@ packages: queue-microtask: 1.2.3 dev: true + /rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + dev: false + /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: @@ -13534,7 +14051,6 @@ packages: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} dependencies: loose-envify: 1.4.0 - dev: true /schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} @@ -13853,6 +14369,7 @@ packages: /socks@2.8.7: resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + requiresBuild: true dependencies: ip-address: 10.1.0 smart-buffer: 4.2.0 @@ -14349,6 +14866,10 @@ packages: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} 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: resolution: {integrity: sha512-GmrB+b6woz6CCdQe6w1GHs/1lt25l7SR5hmhF8jRdarpv/OgjLyuQygLu1makJapixeb1aQhP/Oa1iKi93o/aQ==} dev: true @@ -14468,6 +14989,12 @@ packages: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} 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: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -14647,7 +15174,6 @@ packages: /tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - dev: true /tsutils@3.21.0(typescript@5.2.2): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -15084,6 +15610,12 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} 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: resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} hasBin: true @@ -15509,7 +16041,3 @@ packages: - encoding - supports-color dev: false - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/shell/api.sh b/shell/api.sh index 771a565e..38c3c243 100755 --- a/shell/api.sh +++ b/shell/api.sh @@ -220,4 +220,18 @@ update_auth_config() { 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 diff --git a/shell/otask.sh b/shell/otask.sh index c6612bd1..45a25dc4 100755 --- a/shell/otask.sh +++ b/shell/otask.sh @@ -294,6 +294,7 @@ fi set_u_on="false" check_nounset main "${task_shell_params[@]}" +_task_exit_code=$? if [[ "$set_u_on" == 'true' ]]; then set -u fi @@ -305,4 +306,4 @@ if [[ $isJsOrPythonFile == 'true' ]]; then fi run_task_after "${task_shell_params[@]}" clear_env -handle_task_end "${task_shell_params[@]}" +handle_task_end "${task_shell_params[@]}" "$_task_exit_code" diff --git a/shell/share.sh b/shell/share.sh index 585d885d..1959b7a3 100755 --- a/shell/share.sh +++ b/shell/share.sh @@ -388,18 +388,25 @@ handle_task_end() { local end_time=$(format_time "$time_format" "$etime") local end_timestamp=$(format_timestamp "$time_format" "$etime") local diff_time=$(($end_timestamp - $begin_timestamp)) - local suffix="" - [[ "${MANUAL:=}" == "true" ]] && suffix="(手动停止)" - + local exit_code="${@: -1}" [[ "$diff_time" == 0 ]] && diff_time=1 if [[ $ID ]]; then local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time") if [[ $error ]]; then - error_message=", 任务状态更新失败(${error})" + error_message=", 状态更新失败(${error})" 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 diff --git a/src/layouts/defaultProps.tsx b/src/layouts/defaultProps.tsx index c747b11e..67a70c58 100644 --- a/src/layouts/defaultProps.tsx +++ b/src/layouts/defaultProps.tsx @@ -1,5 +1,5 @@ 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 { BasicLayoutProps } from '@ant-design/pro-layout'; @@ -24,6 +24,12 @@ export default { hideInMenu: true, component: '@/pages/error/index', }, + { + path: '/dashboard', + name: intl.get('统计面板'), + icon: , + component: '@/pages/dashboard/index', + }, { path: '/crontab', name: intl.get('定时任务'), diff --git a/src/layouts/index.less b/src/layouts/index.less index 66a08da5..5103878f 100644 --- a/src/layouts/index.less +++ b/src/layouts/index.less @@ -212,6 +212,7 @@ body { &.crontab-wrapper, &.log-wrapper, &.env-wrapper, + &.dashboard-wrapper, &.config-wrapper { .CodeMirror { width: calc(100vw - 24px); diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 4662e9b7..27a3306a 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -556,5 +556,34 @@ "批量": "Batch", "全局SSH私钥": "Global SSH Private Key", "用于访问所有私有仓库的全局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" } diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 01cdf4ab..2462c147 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -556,5 +556,34 @@ "批量": "批量", "全局SSH私钥": "全局SSH私钥", "用于访问所有私有仓库的全局SSH私钥": "用于访问所有私有仓库的全局SSH私钥", - "请输入完整的SSH私钥内容": "请输入完整的SSH私钥内容" + "请输入完整的SSH私钥内容": "请输入完整的SSH私钥内容", + "统计面板": "统计面板", + "总任务": "总任务", + "今日执行": "今日执行", + "今日成功": "今日成功", + "今日失败": "今日失败", + "成功率": "成功率", + "平均耗时": "平均耗时", + "近 7 日趋势": "近 7 日趋势", + "今日耗时 Top 5": "今日耗时 Top 5", + "今日执行次数 Top 5": "今日执行次数 Top 5", + "实时运行态": "实时运行态", + "系统资源": "系统资源", + "排队中": "排队中", + "暂无运行中任务": "暂无运行中任务", + "暂无数据": "暂无数据", + "总执行": "总执行", + "次数": "次数", + "最长单次": "最长单次", + "已运行": "已运行", + "系统运行": "系统运行", + "内存使用": "内存使用", + "堆内存": "堆内存", + "CPU 核心": "CPU 核心", + "负载 1m": "负载 1m", + "平台": "平台", + "24小时未运行": "24小时未运行", + "标签统计": "标签统计", + "任务数": "任务数", + "内存": "内存" } diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx new file mode 100644 index 00000000..3ad966a6 --- /dev/null +++ b/src/pages/dashboard/index.tsx @@ -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(); + const isDark = theme === 'vs-dark'; + const [overview, setOverview] = useState(null); + const [trend, setTrend] = useState([]); + const [topTime, setTopTime] = useState([]); + const [topCount, setTopCount] = useState([]); + const [runtime, setRuntime] = useState(null); + const [system, setSystem] = useState(null); + const [labels, setLabels] = useState([]); + const [logCron, setLogCron] = useState(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; + 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 ; + + return ( + + + + } /> + + + } /> + + + } /> + + + + + + } /> + + + } /> + + + } /> + + + } /> + + + + + + + {trend.length > 0 ? : } + + + + + + + + v ? `${(v / 1000).toFixed(1)}s` : '-' }, + { title: intl.get('最长单次'), dataIndex: 'maxTime', width: 100, render: (v: number) => v ? `${(v / 1000).toFixed(1)}s` : '-' }, + ]} + /> + + + + +
v ? `${(v / 1000).toFixed(1)}s` : '-' }, + { title: intl.get('成功率'), dataIndex: 'successRate', width: 80, render: (v: string) => `${v}%` }, + ]} + /> + + + + + {labels.length > 0 && ( + + + +
{v} }, + { 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` : '-' }, + ]} + /> + + + + )} + + + + + {intl.get('运行中')} {runtime?.runningCount || 0} + {intl.get('排队中')} {runtime?.queuedCount || 0} + + } + > +
5 ? runtimePagination : false} + size="small" + locale={{ emptyText: }} + 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) => { localStorage.setItem('logCron', String(id)); setLogCron({ id, name: record.name }); }}>{intl.get('查看')} }, + ]} + /> + {runtime?.idleTasks && runtime.idleTasks.length > 0 && ( + <> +
+ {intl.get('24小时未运行')} ({runtime.idleTasks.length}) +
+
{v} }, + ]} + /> + + )} + + + + + + {system && ( + + + { + return `${intl.get('内存')}:${target}%`; + }, + }} + /> + + + + +
+ {intl.get('负载 1m')}: {system.loadAvg?.[0] || '-'} | CPU: {system.cpus} {intl.get('核心')} | {system.platform} +
+ + + )} + + + + {logCron && ( + setLogCron(null)} + /> + )} + + ); +}; + +export default Dashboard; diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 60755c0d..8b0a0d50 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -109,7 +109,7 @@ const Login = () => { ), }); reloadUser(true); - history.push('/crontab'); + history.push('/dashboard'); } else if (code === 410) { setWaitTime(data); } else if (code === 420) { @@ -136,7 +136,7 @@ const Login = () => { useEffect(() => { const isAuth = localStorage.getItem(config.authKey); if (isAuth) { - history.push('/crontab'); + history.push('/dashboard'); } }, []);