mirror of
https://github.com/whyour/qinglong.git
synced 2026-06-13 22:45:10 +08:00
增加运行实例
This commit is contained in:
parent
617cf7e5b4
commit
946731ac8d
|
|
@ -5,6 +5,10 @@ import CronService from '../services/cron';
|
|||
import CronViewService from '../services/cronView';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import { commonCronSchema } from '../validation/schedule';
|
||||
import {
|
||||
RunningInstanceModel,
|
||||
InstanceStatus,
|
||||
} from '../data/runningInstance';
|
||||
|
||||
const route = Router();
|
||||
|
||||
|
|
@ -446,6 +450,49 @@ export default (app: Router) => {
|
|||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/:id/instances',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const instances = await RunningInstanceModel.findAll({
|
||||
where: {
|
||||
cron_id: req.params.id,
|
||||
status: InstanceStatus.running,
|
||||
},
|
||||
order: [['started_at', 'DESC']],
|
||||
raw: true,
|
||||
});
|
||||
return res.send({ code: 200, data: instances });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/:id/instances/:instanceId/stop',
|
||||
celebrate({
|
||||
params: Joi.object({
|
||||
id: Joi.number().required(),
|
||||
instanceId: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request<{ id: number; instanceId: number }>, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.stopInstance(req.params.instanceId);
|
||||
return res.send(data);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/:id/logs',
|
||||
celebrate({
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import { Container } from 'typedi';
|
|||
import { fn, col, where, Op } from 'sequelize';
|
||||
import { CrontabModel } from '../data/cron';
|
||||
import { CrontabStatModel } from '../data/cronStats';
|
||||
import {
|
||||
RunningInstanceModel,
|
||||
InstanceStatus,
|
||||
} from '../data/runningInstance';
|
||||
import dayjs from 'dayjs';
|
||||
import os from 'os';
|
||||
|
||||
|
|
@ -239,9 +243,9 @@ export default (app: Router) => {
|
|||
'/runtime',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const runningCrons = await CrontabModel.findAll({
|
||||
const runningInstances = await RunningInstanceModel.findAll({
|
||||
where: {
|
||||
status: 0, // running
|
||||
status: InstanceStatus.running,
|
||||
},
|
||||
raw: true,
|
||||
});
|
||||
|
|
@ -253,15 +257,31 @@ export default (app: Router) => {
|
|||
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,
|
||||
}));
|
||||
// Fetch cron names for running instances
|
||||
const cronIds = [
|
||||
...new Set(runningInstances.map((i: any) => i.cron_id)),
|
||||
];
|
||||
const crons =
|
||||
cronIds.length > 0
|
||||
? await CrontabModel.findAll({
|
||||
where: { id: cronIds },
|
||||
raw: true,
|
||||
})
|
||||
: [];
|
||||
const cronMap = new Map(crons.map((c: any) => [c.id, c]));
|
||||
|
||||
const now = dayjs().unix();
|
||||
const running = runningInstances.map((inst: any) => {
|
||||
const cron = cronMap.get(inst.cron_id);
|
||||
return {
|
||||
instanceId: inst.id,
|
||||
id: inst.cron_id,
|
||||
name: cron?.name || cron?.command || `任务#${inst.cron_id}`,
|
||||
pid: inst.pid,
|
||||
elapsed: inst.started_at ? now - inst.started_at : 0,
|
||||
logPath: inst.log_path,
|
||||
};
|
||||
});
|
||||
|
||||
const dayAgo = dayjs().subtract(24, 'hour').unix();
|
||||
const idleTasks = await CrontabModel.findAll({
|
||||
|
|
|
|||
81
back/data/runningInstance.ts
Normal file
81
back/data/runningInstance.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { sequelize } from '.';
|
||||
import { DataTypes, Model } from 'sequelize';
|
||||
|
||||
export enum InstanceStatus {
|
||||
'running' = 0,
|
||||
'finished' = 1,
|
||||
'stopped' = 2,
|
||||
'error' = 3,
|
||||
}
|
||||
|
||||
export interface RunningInstanceAttributes {
|
||||
id?: number;
|
||||
cron_id: number;
|
||||
pid?: number;
|
||||
log_path?: string;
|
||||
started_at: number;
|
||||
finished_at?: number;
|
||||
status: InstanceStatus;
|
||||
exit_code?: number;
|
||||
}
|
||||
|
||||
export class RunningInstance {
|
||||
id?: number;
|
||||
cron_id!: number;
|
||||
pid?: number;
|
||||
log_path?: string;
|
||||
started_at!: number;
|
||||
finished_at?: number;
|
||||
status!: InstanceStatus;
|
||||
exit_code?: number;
|
||||
|
||||
constructor(options: RunningInstanceAttributes) {
|
||||
this.id = options.id;
|
||||
this.cron_id = options.cron_id;
|
||||
this.pid = options.pid;
|
||||
this.log_path = options.log_path;
|
||||
this.started_at = options.started_at;
|
||||
this.finished_at = options.finished_at;
|
||||
this.status = options.status;
|
||||
this.exit_code = options.exit_code;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RunningInstanceModel
|
||||
extends Model<RunningInstanceAttributes, RunningInstanceAttributes>,
|
||||
RunningInstanceAttributes {}
|
||||
|
||||
export const RunningInstanceModel = sequelize.define<RunningInstanceModel>(
|
||||
'RunningInstance',
|
||||
{
|
||||
cron_id: {
|
||||
type: DataTypes.NUMBER,
|
||||
allowNull: false,
|
||||
},
|
||||
pid: {
|
||||
type: DataTypes.NUMBER,
|
||||
allowNull: true,
|
||||
},
|
||||
log_path: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
started_at: {
|
||||
type: DataTypes.NUMBER,
|
||||
allowNull: false,
|
||||
},
|
||||
finished_at: {
|
||||
type: DataTypes.NUMBER,
|
||||
allowNull: true,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.NUMBER,
|
||||
allowNull: false,
|
||||
defaultValue: InstanceStatus.running,
|
||||
},
|
||||
exit_code: {
|
||||
type: DataTypes.NUMBER,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -7,6 +7,7 @@ import { SystemModel } from '../data/system';
|
|||
import { SubscriptionModel } from '../data/subscription';
|
||||
import { CrontabViewModel } from '../data/cronView';
|
||||
import { CrontabStatModel } from '../data/cronStats';
|
||||
import { RunningInstanceModel } from '../data/runningInstance';
|
||||
import { sequelize } from '../data';
|
||||
|
||||
export default async () => {
|
||||
|
|
@ -19,6 +20,7 @@ export default async () => {
|
|||
await SubscriptionModel.sync();
|
||||
await CrontabViewModel.sync();
|
||||
await CrontabStatModel.sync();
|
||||
await RunningInstanceModel.sync();
|
||||
|
||||
// 初始化新增字段
|
||||
const migrations = [
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import OpenService from '../services/open';
|
|||
import { shareStore } from '../shared/store';
|
||||
import Logger from './logger';
|
||||
import { AppModel } from '../data/open';
|
||||
import { InstanceStatus, RunningInstanceModel } from '../data/runningInstance';
|
||||
|
||||
export default async () => {
|
||||
const cronService = Container.get(CronService);
|
||||
|
|
@ -139,6 +140,12 @@ export default async () => {
|
|||
// 初始化更新所有任务状态为空闲
|
||||
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
|
||||
|
||||
// 清空所有运行中的实例记录(服务重启后进程已不存在)
|
||||
await RunningInstanceModel.update(
|
||||
{ status: InstanceStatus.stopped },
|
||||
{ where: { status: InstanceStatus.running } },
|
||||
);
|
||||
|
||||
// 初始化时执行一次所有的 ql repo 任务
|
||||
CrontabModel.findAll({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import { Service, Inject } from 'typedi';
|
|||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||
import {
|
||||
RunningInstanceModel,
|
||||
InstanceStatus,
|
||||
} from '../data/runningInstance';
|
||||
import { exec, execSync } from 'child_process';
|
||||
import fs from 'fs/promises';
|
||||
import CronExpressionParser from 'cron-parser';
|
||||
|
|
@ -189,6 +193,35 @@ export default class CronService {
|
|||
if (status === CrontabStatus.idle && log_path !== cron.log_path) {
|
||||
options = omit(options, ['status', 'log_path', 'pid']);
|
||||
}
|
||||
|
||||
// Manage RunningInstance records for status transitions from shell scripts
|
||||
if (status === CrontabStatus.running) {
|
||||
// Create a new running instance record
|
||||
await RunningInstanceModel.create({
|
||||
cron_id: id,
|
||||
pid: pid || undefined,
|
||||
log_path: log_path || undefined,
|
||||
started_at: last_execution_time || dayjs().unix(),
|
||||
status: InstanceStatus.running,
|
||||
});
|
||||
} else if (status === CrontabStatus.idle) {
|
||||
// Mark the matching running instance as finished
|
||||
const finishedAt = dayjs().unix();
|
||||
await RunningInstanceModel.update(
|
||||
{
|
||||
finished_at: finishedAt,
|
||||
status: InstanceStatus.finished,
|
||||
},
|
||||
{
|
||||
where: {
|
||||
cron_id: id,
|
||||
pid: pid || undefined,
|
||||
status: InstanceStatus.running,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await CrontabModel.update(
|
||||
{ ...pickBy(options, (v) => v === 0 || !!v) },
|
||||
{ where: { id } },
|
||||
|
|
@ -499,12 +532,53 @@ export default class CronService {
|
|||
}
|
||||
}
|
||||
|
||||
// Mark all running instances as stopped
|
||||
const finishedAt = dayjs().unix();
|
||||
await RunningInstanceModel.update(
|
||||
{ status: InstanceStatus.stopped, finished_at: finishedAt },
|
||||
{ where: { cron_id: ids, status: InstanceStatus.running } },
|
||||
);
|
||||
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id: ids } },
|
||||
);
|
||||
}
|
||||
|
||||
public async stopInstance(instanceId: number) {
|
||||
const instance = await RunningInstanceModel.findOne({
|
||||
where: { id: instanceId, status: InstanceStatus.running },
|
||||
});
|
||||
if (!instance) {
|
||||
return { code: 400, message: '实例不存在或已停止' };
|
||||
}
|
||||
if (instance.pid) {
|
||||
try {
|
||||
await killTask(instance.pid);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[panel][停止实例失败] 实例ID: ${instanceId}, PID: ${instance.pid}, 错误: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
await RunningInstanceModel.update(
|
||||
{ status: InstanceStatus.stopped, finished_at: dayjs().unix() },
|
||||
{ where: { id: instanceId } },
|
||||
);
|
||||
|
||||
// Check if there are still other running instances for this cron
|
||||
const otherRunning = await RunningInstanceModel.count({
|
||||
where: { cron_id: instance.cron_id, status: InstanceStatus.running },
|
||||
});
|
||||
if (otherRunning === 0) {
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id: instance.cron_id } },
|
||||
);
|
||||
}
|
||||
return { code: 200, message: '实例已停止' };
|
||||
}
|
||||
|
||||
private async runSingle(cronId: number): Promise<number | void> {
|
||||
return taskLimit.manualRunWithCronLimit(() => {
|
||||
return new Promise(async (resolve: any) => {
|
||||
|
|
@ -543,6 +617,15 @@ export default class CronService {
|
|||
{ shell: '/bin/bash' },
|
||||
);
|
||||
|
||||
const startedAt = dayjs().unix();
|
||||
const instance = await RunningInstanceModel.create({
|
||||
cron_id: id!,
|
||||
pid: cp.pid,
|
||||
log_path: logPath,
|
||||
started_at: startedAt,
|
||||
status: InstanceStatus.running,
|
||||
});
|
||||
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
|
||||
{ where: { id } },
|
||||
|
|
@ -574,10 +657,26 @@ export default class CronService {
|
|||
code,
|
||||
);
|
||||
await logStreamManager.closeStream(absolutePath);
|
||||
const finishedAt = dayjs().unix();
|
||||
await RunningInstanceModel.update(
|
||||
{
|
||||
finished_at: finishedAt,
|
||||
status: code === 0 ? InstanceStatus.finished : InstanceStatus.error,
|
||||
exit_code: code ?? undefined,
|
||||
},
|
||||
{ where: { id: instance.id } },
|
||||
);
|
||||
|
||||
// Only set cron to idle if no other running instances exist
|
||||
const otherRunning = await RunningInstanceModel.count({
|
||||
where: { cron_id: id!, status: InstanceStatus.running },
|
||||
});
|
||||
if (otherRunning === 0) {
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id } },
|
||||
);
|
||||
}
|
||||
resolve({ ...params, pid: cp.pid, code });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ import Logger from '../loaders/logger';
|
|||
import { ICron } from '../protos/cron';
|
||||
import { CrontabModel, CrontabStatus } from '../data/cron';
|
||||
import { killTask } from '../config/util';
|
||||
import {
|
||||
RunningInstanceModel,
|
||||
InstanceStatus,
|
||||
} from '../data/runningInstance';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
||||
return taskLimit.runWithCronLimit(cron, () => {
|
||||
|
|
@ -29,6 +34,12 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
|||
`[schedule][停止已运行任务] 任务ID: ${cron.id}, PID: ${existingCron.pid}`,
|
||||
);
|
||||
await killTask(existingCron.pid);
|
||||
// Mark old running instances as stopped
|
||||
const stoppedAt = dayjs().unix();
|
||||
await RunningInstanceModel.update(
|
||||
{ status: InstanceStatus.stopped, finished_at: stoppedAt },
|
||||
{ where: { cron_id: Number(cron.id), status: InstanceStatus.running } },
|
||||
);
|
||||
// Update the status to idle after killing
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -22,6 +22,7 @@ import {
|
|||
PlayCircleOutlined,
|
||||
PauseCircleOutlined,
|
||||
FullscreenOutlined,
|
||||
StopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { CrontabStatus } from './type';
|
||||
import { diffTime } from '@/utils/date';
|
||||
|
|
@ -46,6 +47,10 @@ const tabList = [
|
|||
key: 'script',
|
||||
tab: intl.get('脚本'),
|
||||
},
|
||||
{
|
||||
key: 'runningInstance',
|
||||
tab: intl.get('运行实例'),
|
||||
},
|
||||
];
|
||||
|
||||
interface LogItem {
|
||||
|
|
@ -77,6 +82,36 @@ const CronDetailModal = ({
|
|||
const [currentCron, setCurrentCron] = useState<any>({});
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const tableScrollHeight = useScrollHeight(listRef);
|
||||
const [runningInstances, setRunningInstances] = useState<any[]>([]);
|
||||
const needRefreshRef = useRef(false);
|
||||
|
||||
const fetchRunningInstances = async () => {
|
||||
if (!cron.id) return Promise.resolve();
|
||||
return request
|
||||
.get(`${config.apiPrefix}crons/${cron.id}/instances`)
|
||||
.then(({ code, data }) => {
|
||||
if (code === 200 && data) {
|
||||
setRunningInstances(data);
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRunningInstances();
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
let cancelled = false;
|
||||
const poll = async () => {
|
||||
await fetchRunningInstances();
|
||||
if (cancelled) return;
|
||||
timer = setTimeout(poll, 10000);
|
||||
};
|
||||
poll();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [cron.id]);
|
||||
|
||||
const contentList: any = {
|
||||
log: (
|
||||
|
|
@ -115,11 +150,106 @@ const CronDetailModal = ({
|
|||
}}
|
||||
/>
|
||||
),
|
||||
runningInstance: (
|
||||
<div ref={listRef}>
|
||||
<List>
|
||||
<VirtualList
|
||||
data={runningInstances}
|
||||
height={tableScrollHeight}
|
||||
itemHeight={47}
|
||||
itemKey="id"
|
||||
>
|
||||
{(item) => (
|
||||
<List.Item
|
||||
className="log-item"
|
||||
actions={[
|
||||
<Tooltip title={intl.get('查看日志')} key="log">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<FileOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
viewInstanceLog(item);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title={intl.get('停止')} key="stop">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<StopOutlined />}
|
||||
danger
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
stopRunningInstance(item);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<span>
|
||||
PID: <Tag color="processing">{item.pid}</Tag>
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
<span style={{ color: '#999' }}>
|
||||
{intl.get('启动')}: {dayjs.unix(item.started_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
</VirtualList>
|
||||
</List>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
const stopRunningInstance = (instance: any) => {
|
||||
Modal.confirm({
|
||||
title: intl.get('确认停止实例'),
|
||||
content: (
|
||||
<>
|
||||
{intl.get('确认停止运行实例')} PID: {instance.pid}{' '}
|
||||
{intl.get('吗')}
|
||||
</>
|
||||
),
|
||||
onOk() {
|
||||
request
|
||||
.post(`${config.apiPrefix}crons/${cron.id}/instances/${instance.id}/stop`)
|
||||
.then(({ code }) => {
|
||||
if (code === 200) {
|
||||
message.success(intl.get('实例已停止'));
|
||||
needRefreshRef.current = true;
|
||||
fetchRunningInstances();
|
||||
setTimeout(() => getLogs(), 1000);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const viewInstanceLog = (instance: any) => {
|
||||
if (!instance.log_path) return;
|
||||
const parts = instance.log_path.split('/');
|
||||
const filename = parts.pop() || '';
|
||||
const directory = parts.join('/');
|
||||
const url = `${config.apiPrefix}logs/detail?file=${filename}&path=${directory}`;
|
||||
localStorage.setItem('logCron', url);
|
||||
setLogUrl(url);
|
||||
request.get(url).then(({ code, data }) => {
|
||||
if (code === 200) {
|
||||
setLog(data);
|
||||
setIsLogModalVisible(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onClickItem = (item: LogItem) => {
|
||||
const url = `${config.apiPrefix}logs/detail?file=${item.filename}&path=${
|
||||
item.directory || ''
|
||||
const url = `${config.apiPrefix}logs/detail?file=${item.filename}&path=${item.directory || ''
|
||||
}`;
|
||||
localStorage.setItem('logCron', url);
|
||||
setLogUrl(url);
|
||||
|
|
@ -256,8 +386,7 @@ const CronDetailModal = ({
|
|||
|
||||
const enabledOrDisabledCron = () => {
|
||||
Modal.confirm({
|
||||
title: `确认${
|
||||
currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
|
||||
title: `确认${currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
|
||||
}`,
|
||||
content: (
|
||||
<>
|
||||
|
|
@ -273,8 +402,7 @@ const CronDetailModal = ({
|
|||
onOk() {
|
||||
request
|
||||
.put(
|
||||
`${config.apiPrefix}crons/${
|
||||
currentCron.isDisabled === 1 ? 'enable' : 'disable'
|
||||
`${config.apiPrefix}crons/${currentCron.isDisabled === 1 ? 'enable' : 'disable'
|
||||
}`,
|
||||
[currentCron.id],
|
||||
)
|
||||
|
|
@ -292,8 +420,7 @@ const CronDetailModal = ({
|
|||
|
||||
const pinOrUnPinCron = () => {
|
||||
Modal.confirm({
|
||||
title: `确认${
|
||||
currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
|
||||
title: `确认${currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
|
||||
}`,
|
||||
content: (
|
||||
<>
|
||||
|
|
@ -309,8 +436,7 @@ const CronDetailModal = ({
|
|||
onOk() {
|
||||
request
|
||||
.put(
|
||||
`${config.apiPrefix}crons/${
|
||||
currentCron.isPinned === 1 ? 'unpin' : 'pin'
|
||||
`${config.apiPrefix}crons/${currentCron.isPinned === 1 ? 'unpin' : 'pin'
|
||||
}`,
|
||||
[currentCron.id],
|
||||
)
|
||||
|
|
@ -441,7 +567,7 @@ const CronDetailModal = ({
|
|||
open={true}
|
||||
forceRender
|
||||
footer={false}
|
||||
onCancel={() => handleCancel()}
|
||||
onCancel={() => handleCancel(needRefreshRef.current)}
|
||||
wrapClassName="crontab-detail"
|
||||
width={!isPhone ? '80vw' : ''}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -365,8 +365,8 @@ const Crontab = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const getCrons = () => {
|
||||
setLoading(true);
|
||||
const getCrons = async (silent?: boolean) => {
|
||||
if (!silent) setLoading(true);
|
||||
const { page, size, sorter, filters } = pageConf;
|
||||
let url = `${config.apiPrefix
|
||||
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
|
||||
|
|
@ -385,7 +385,7 @@ const Crontab = () => {
|
|||
filterRelation: viewConf.filterRelation || 'and',
|
||||
})}`;
|
||||
}
|
||||
request
|
||||
return request
|
||||
.get(url)
|
||||
.then(async ({ code, data: _data }) => {
|
||||
if (code === 200) {
|
||||
|
|
@ -419,9 +419,24 @@ const Crontab = () => {
|
|||
setTotal(total);
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
.finally(() => !silent && setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
let cancelled = false;
|
||||
const poll = async () => {
|
||||
await getCrons(true);
|
||||
if (cancelled) return;
|
||||
timer = setTimeout(poll, 10000);
|
||||
};
|
||||
poll();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [pageConf, viewConf, searchText]);
|
||||
|
||||
const addCron = () => {
|
||||
setEditedCron(null as any);
|
||||
setIsModalVisible(true);
|
||||
|
|
@ -809,7 +824,7 @@ const Crontab = () => {
|
|||
setAllSubscriptions(data || []);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => { });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -1077,8 +1092,11 @@ const Crontab = () => {
|
|||
)}
|
||||
{isDetailModalVisible && (
|
||||
<CronDetailModal
|
||||
handleCancel={() => {
|
||||
handleCancel={(needUpdate?: boolean) => {
|
||||
setIsDetailModalVisible(false);
|
||||
if (needUpdate) {
|
||||
getCrons();
|
||||
}
|
||||
}}
|
||||
cron={detailCron}
|
||||
theme={theme}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ interface TopItem {
|
|||
interface Runtime {
|
||||
runningCount: number;
|
||||
queuedCount: number;
|
||||
running: Array<{ id: number; name: string; pid: number; elapsed: number; logPath: string }>;
|
||||
running: Array<{ instanceId: number; id: number; name: string; pid: number; elapsed: number; logPath: string }>;
|
||||
idleTasks: Array<{ id: number; name: string; lastRun: string }>;
|
||||
}
|
||||
|
||||
|
|
@ -282,12 +282,15 @@ const Dashboard = () => {
|
|||
>
|
||||
<Table
|
||||
dataSource={runtime?.running || []}
|
||||
rowKey="id"
|
||||
rowKey="instanceId"
|
||||
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: intl.get('定时任务'), dataIndex: 'name', ellipsis: true, render: (name: string, record) => {
|
||||
const sameTaskCount = (runtime?.running || []).filter(r => r.id === record.id).length;
|
||||
return sameTaskCount > 1 ? <span>{name} <Tag color="processing" style={{ fontSize: 10, lineHeight: '16px' }}>×{sameTaskCount}</Tag></span> : name;
|
||||
} },
|
||||
{ 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> },
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user