增加运行实例

This commit is contained in:
whyour
2026-06-10 01:53:10 +08:00
parent 617cf7e5b4
commit 946731ac8d
12 changed files with 1594 additions and 1168 deletions
+560 -554
View File
File diff suppressed because it is too large Load Diff
+560 -554
View File
File diff suppressed because it is too large Load Diff
+163 -37
View File
@@ -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,12 +150,107 @@ 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);
request.get(url).then(({ code, data }) => {
@@ -256,9 +386,8 @@ const CronDetailModal = ({
const enabledOrDisabledCron = () => {
Modal.confirm({
title: `确认${
currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
}`,
title: `确认${currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
}`,
content: (
<>
{intl.get('确认')}
@@ -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,9 +420,8 @@ const CronDetailModal = ({
const pinOrUnPinCron = () => {
Modal.confirm({
title: `确认${
currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
}`,
title: `确认${currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
}`,
content: (
<>
{intl.get('确认')}
@@ -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' : ''}
>
@@ -458,27 +584,27 @@ const CronDetailModal = ({
<div className="cron-detail-info-value">
{(!currentCron.isDisabled ||
currentCron.status !== CrontabStatus.idle) && (
<>
{currentCron.status === CrontabStatus.idle && (
<Tag icon={<ClockCircleOutlined />} color="default">
{intl.get('空闲中')}
</Tag>
)}
{currentCron.status === CrontabStatus.running && (
<Tag
icon={<Loading3QuartersOutlined spin />}
color="processing"
>
{intl.get('运行中')}
</Tag>
)}
{currentCron.status === CrontabStatus.queued && (
<Tag icon={<FieldTimeOutlined />} color="default">
{intl.get('队列中')}
</Tag>
)}
</>
)}
<>
{currentCron.status === CrontabStatus.idle && (
<Tag icon={<ClockCircleOutlined />} color="default">
{intl.get('空闲中')}
</Tag>
)}
{currentCron.status === CrontabStatus.running && (
<Tag
icon={<Loading3QuartersOutlined spin />}
color="processing"
>
{intl.get('运行中')}
</Tag>
)}
{currentCron.status === CrontabStatus.queued && (
<Tag icon={<FieldTimeOutlined />} color="default">
{intl.get('队列中')}
</Tag>
)}
</>
)}
{currentCron.isDisabled === 1 &&
currentCron.status === CrontabStatus.idle && (
<Tag icon={<CloseCircleOutlined />} color="error">
@@ -503,8 +629,8 @@ const CronDetailModal = ({
<div className="cron-detail-info-value">
{currentCron.last_execution_time
? dayjs(currentCron.last_execution_time * 1000).format(
'YYYY-MM-DD HH:mm:ss',
)
'YYYY-MM-DD HH:mm:ss',
)
: '-'}
</div>
</div>
+24 -6
View File
@@ -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}
+6 -3
View File
@@ -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> },