mirror of
https://github.com/whyour/qinglong.git
synced 2026-07-17 09:34:31 +08:00
增加运行实例
This commit is contained in:
+560
-554
File diff suppressed because it is too large
Load Diff
+560
-554
File diff suppressed because it is too large
Load Diff
+163
-37
@@ -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>
|
||||
|
||||
@@ -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> },
|
||||
|
||||
Reference in New Issue
Block a user