mirror of
https://github.com/whyour/qinglong.git
synced 2025-11-09 16:16:07 +08:00
Add frontend UI for Scenario Mode with i18n support
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
712ff80448
commit
37c8e28cba
|
|
@ -66,6 +66,12 @@ export default {
|
|||
icon: <IconFont type="ql-icon-log" />,
|
||||
component: '@/pages/log/index',
|
||||
},
|
||||
{
|
||||
path: '/scenario',
|
||||
name: intl.get('场景模式'),
|
||||
icon: <IconFont type="ql-icon-scenario" />,
|
||||
component: '@/pages/scenario/index',
|
||||
},
|
||||
{
|
||||
path: '/diff',
|
||||
name: intl.get('对比工具'),
|
||||
|
|
|
|||
|
|
@ -521,5 +521,75 @@
|
|||
"远程仓库缓存": "Remote repository cache",
|
||||
"SSH 文件缓存": "SSH file cache",
|
||||
"清除依赖缓存": "Clean dependency cache",
|
||||
"清除成功": "Clean successful"
|
||||
"清除成功": "Clean successful",
|
||||
"场景模式": "Scenario Mode",
|
||||
"新建场景": "New Scenario",
|
||||
"编辑场景": "Edit Scenario",
|
||||
"场景日志": "Scenario Logs",
|
||||
"确定要删除场景": "Are you sure to delete scenario",
|
||||
"场景已触发": "Scenario triggered",
|
||||
"使用此 URL 接收外部触发": "Use this URL to receive external triggers",
|
||||
"获取 Webhook URL 失败": "Failed to get Webhook URL",
|
||||
"获取 Webhook": "Get Webhook",
|
||||
"手动触发": "Manual Trigger",
|
||||
"查看日志": "View Logs",
|
||||
"变量监听": "Variable Monitor",
|
||||
"任务状态": "Task Status",
|
||||
"时间触发": "Time Trigger",
|
||||
"系统事件": "System Event",
|
||||
"描述": "Description",
|
||||
"触发类型": "Trigger Type",
|
||||
"执行次数": "Execution Count",
|
||||
"成功/失败": "Success/Failure",
|
||||
"最后触发": "Last Triggered",
|
||||
"监听路径": "Watch Path",
|
||||
"留空自动生成": "Leave blank to auto-generate",
|
||||
"Cron 表达式": "Cron Expression",
|
||||
"事件类型": "Event Type",
|
||||
"磁盘空间": "Disk Space",
|
||||
"内存使用": "Memory Usage",
|
||||
"阈值": "Threshold",
|
||||
"检查间隔": "Check Interval",
|
||||
"任务 ID": "Task ID",
|
||||
"条件配置": "Condition Configuration",
|
||||
"条件逻辑": "Condition Logic",
|
||||
"多个条件之间的关系": "Relationship between multiple conditions",
|
||||
"全部满足": "All satisfied",
|
||||
"任一满足": "Any satisfied",
|
||||
"字段名": "Field Name",
|
||||
"操作符": "Operator",
|
||||
"包含": "Contains",
|
||||
"不包含": "Not Contains",
|
||||
"值": "Value",
|
||||
"添加条件": "Add Condition",
|
||||
"动作配置": "Action Configuration",
|
||||
"动作类型": "Action Type",
|
||||
"运行任务": "Run Task",
|
||||
"设置变量": "Set Variable",
|
||||
"执行命令": "Execute Command",
|
||||
"发送通知": "Send Notification",
|
||||
"变量名": "Variable Name",
|
||||
"变量值": "Variable Value",
|
||||
"命令": "Command",
|
||||
"消息": "Message",
|
||||
"添加动作": "Add Action",
|
||||
"高级设置": "Advanced Settings",
|
||||
"延迟执行": "Delay Execution",
|
||||
"秒": "seconds",
|
||||
"失败熔断阈值": "Failure Threshold",
|
||||
"连续失败多少次后自动禁用": "Auto-disable after N consecutive failures",
|
||||
"最大重试次数": "Max Retry Count",
|
||||
"重试延迟": "Retry Delay",
|
||||
"退避倍数": "Backoff Multiplier",
|
||||
"每次重试延迟的乘数": "Multiplier for retry delay",
|
||||
"启用": "Enable",
|
||||
"时间": "Time",
|
||||
"条件匹配": "Condition Matched",
|
||||
"执行时间": "Execution Time",
|
||||
"重试次数": "Retry Count",
|
||||
"错误信息": "Error Message",
|
||||
"是": "Yes",
|
||||
"否": "No",
|
||||
"共": "Total",
|
||||
"项": "items"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -521,5 +521,75 @@
|
|||
"远程仓库缓存": "远程仓库缓存",
|
||||
"SSH 文件缓存": "SSH 文件缓存",
|
||||
"清除依赖缓存": "清除依赖缓存",
|
||||
"清除成功": "清除成功"
|
||||
"清除成功": "清除成功",
|
||||
"场景模式": "场景模式",
|
||||
"新建场景": "新建场景",
|
||||
"编辑场景": "编辑场景",
|
||||
"场景日志": "场景日志",
|
||||
"确定要删除场景": "确定要删除场景",
|
||||
"场景已触发": "场景已触发",
|
||||
"使用此 URL 接收外部触发": "使用此 URL 接收外部触发",
|
||||
"获取 Webhook URL 失败": "获取 Webhook URL 失败",
|
||||
"获取 Webhook": "获取 Webhook",
|
||||
"手动触发": "手动触发",
|
||||
"查看日志": "查看日志",
|
||||
"变量监听": "变量监听",
|
||||
"任务状态": "任务状态",
|
||||
"时间触发": "时间触发",
|
||||
"系统事件": "系统事件",
|
||||
"描述": "描述",
|
||||
"触发类型": "触发类型",
|
||||
"执行次数": "执行次数",
|
||||
"成功/失败": "成功/失败",
|
||||
"最后触发": "最后触发",
|
||||
"监听路径": "监听路径",
|
||||
"留空自动生成": "留空自动生成",
|
||||
"Cron 表达式": "Cron 表达式",
|
||||
"事件类型": "事件类型",
|
||||
"磁盘空间": "磁盘空间",
|
||||
"内存使用": "内存使用",
|
||||
"阈值": "阈值",
|
||||
"检查间隔": "检查间隔",
|
||||
"任务 ID": "任务 ID",
|
||||
"条件配置": "条件配置",
|
||||
"条件逻辑": "条件逻辑",
|
||||
"多个条件之间的关系": "多个条件之间的关系",
|
||||
"全部满足": "全部满足",
|
||||
"任一满足": "任一满足",
|
||||
"字段名": "字段名",
|
||||
"操作符": "操作符",
|
||||
"包含": "包含",
|
||||
"不包含": "不包含",
|
||||
"值": "值",
|
||||
"添加条件": "添加条件",
|
||||
"动作配置": "动作配置",
|
||||
"动作类型": "动作类型",
|
||||
"运行任务": "运行任务",
|
||||
"设置变量": "设置变量",
|
||||
"执行命令": "执行命令",
|
||||
"发送通知": "发送通知",
|
||||
"变量名": "变量名",
|
||||
"变量值": "变量值",
|
||||
"命令": "命令",
|
||||
"消息": "消息",
|
||||
"添加动作": "添加动作",
|
||||
"高级设置": "高级设置",
|
||||
"延迟执行": "延迟执行",
|
||||
"秒": "秒",
|
||||
"失败熔断阈值": "失败熔断阈值",
|
||||
"连续失败多少次后自动禁用": "连续失败多少次后自动禁用",
|
||||
"最大重试次数": "最大重试次数",
|
||||
"重试延迟": "重试延迟",
|
||||
"退避倍数": "退避倍数",
|
||||
"每次重试延迟的乘数": "每次重试延迟的乘数",
|
||||
"启用": "启用",
|
||||
"时间": "时间",
|
||||
"条件匹配": "条件匹配",
|
||||
"执行时间": "执行时间",
|
||||
"重试次数": "重试次数",
|
||||
"错误信息": "错误信息",
|
||||
"是": "是",
|
||||
"否": "否",
|
||||
"共": "共",
|
||||
"项": "项"
|
||||
}
|
||||
|
|
|
|||
318
src/pages/scenario/index.tsx
Normal file
318
src/pages/scenario/index.tsx
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Space,
|
||||
Modal,
|
||||
message,
|
||||
Tag,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Dropdown,
|
||||
MenuProps,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
PlayCircleOutlined,
|
||||
EllipsisOutlined,
|
||||
FileTextOutlined,
|
||||
LinkOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { request } from '@/utils/http';
|
||||
import intl from 'react-intl-universal';
|
||||
import ScenarioModal from './modal';
|
||||
import ScenarioLogModal from './logModal';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const Scenario = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [scenarios, setScenarios] = useState<any[]>([]);
|
||||
const [selectedScenario, setSelectedScenario] = useState<any>(null);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||
|
||||
const fetchScenarios = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { code, data } = await request.get('/api/scenarios');
|
||||
if (code === 200) {
|
||||
setScenarios(data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch scenarios:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchScenarios();
|
||||
}, []);
|
||||
|
||||
const handleCreate = () => {
|
||||
setSelectedScenario(null);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
setSelectedScenario(record);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: intl.get('确认删除'),
|
||||
content: `${intl.get('确定要删除场景')} "${record.name}" ${intl.get('吗')}?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const { code } = await request.delete('/api/scenarios', {
|
||||
data: [record.id],
|
||||
});
|
||||
if (code === 200) {
|
||||
message.success(intl.get('删除成功'));
|
||||
fetchScenarios();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete scenario:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleEnabled = async (record: any) => {
|
||||
try {
|
||||
const { code } = await request.put('/api/scenarios', {
|
||||
id: record.id,
|
||||
isEnabled: record.isEnabled === 1 ? 0 : 1,
|
||||
});
|
||||
if (code === 200) {
|
||||
message.success(intl.get('更新成功'));
|
||||
fetchScenarios();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle scenario:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTrigger = async (record: any) => {
|
||||
try {
|
||||
const { code } = await request.post(`/api/scenarios/${record.id}/trigger`, {});
|
||||
if (code === 200) {
|
||||
message.success(intl.get('场景已触发'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to trigger scenario:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewLogs = (record: any) => {
|
||||
setSelectedScenario(record);
|
||||
setIsLogModalVisible(true);
|
||||
};
|
||||
|
||||
const handleGetWebhook = async (record: any) => {
|
||||
try {
|
||||
const { code, data } = await request.get(`/api/scenarios/${record.id}/webhook`);
|
||||
if (code === 200) {
|
||||
Modal.info({
|
||||
title: 'Webhook URL',
|
||||
content: (
|
||||
<div>
|
||||
<p>{intl.get('使用此 URL 接收外部触发')}:</p>
|
||||
<code style={{ wordBreak: 'break-all' }}>{data.webhookUrl}</code>
|
||||
</div>
|
||||
),
|
||||
width: 600,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(intl.get('获取 Webhook URL 失败'));
|
||||
}
|
||||
};
|
||||
|
||||
const getTriggerTypeName = (type: string) => {
|
||||
const types: any = {
|
||||
variable: intl.get('变量监听'),
|
||||
webhook: 'Webhook',
|
||||
task_status: intl.get('任务状态'),
|
||||
time: intl.get('时间触发'),
|
||||
system_event: intl.get('系统事件'),
|
||||
};
|
||||
return types[type] || type;
|
||||
};
|
||||
|
||||
const getActionsMenu = (record: any): MenuProps => ({
|
||||
items: [
|
||||
{
|
||||
key: 'trigger',
|
||||
icon: <PlayCircleOutlined />,
|
||||
label: intl.get('手动触发'),
|
||||
onClick: () => handleTrigger(record),
|
||||
},
|
||||
{
|
||||
key: 'logs',
|
||||
icon: <FileTextOutlined />,
|
||||
label: intl.get('查看日志'),
|
||||
onClick: () => handleViewLogs(record),
|
||||
},
|
||||
...(record.triggerType === 'webhook'
|
||||
? [
|
||||
{
|
||||
key: 'webhook',
|
||||
icon: <LinkOutlined />,
|
||||
label: intl.get('获取 Webhook'),
|
||||
onClick: () => handleGetWebhook(record),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
label: intl.get('编辑'),
|
||||
onClick: () => handleEdit(record),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
label: intl.get('删除'),
|
||||
danger: true,
|
||||
onClick: () => handleDelete(record),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: intl.get('名称'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: intl.get('描述'),
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: intl.get('触发类型'),
|
||||
dataIndex: 'triggerType',
|
||||
key: 'triggerType',
|
||||
width: 120,
|
||||
render: (type: string) => (
|
||||
<Tag color="blue">{getTriggerTypeName(type)}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: intl.get('状态'),
|
||||
dataIndex: 'isEnabled',
|
||||
key: 'isEnabled',
|
||||
width: 80,
|
||||
render: (isEnabled: number, record: any) => (
|
||||
<Switch
|
||||
checked={isEnabled === 1}
|
||||
onChange={() => handleToggleEnabled(record)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: intl.get('执行次数'),
|
||||
dataIndex: 'executionCount',
|
||||
key: 'executionCount',
|
||||
width: 100,
|
||||
render: (count: number) => count || 0,
|
||||
},
|
||||
{
|
||||
title: intl.get('成功/失败'),
|
||||
key: 'stats',
|
||||
width: 100,
|
||||
render: (_: any, record: any) => (
|
||||
<span>
|
||||
<Tag color="success">{record.successCount || 0}</Tag>
|
||||
<Tag color="error">{record.failureCount || 0}</Tag>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: intl.get('最后触发'),
|
||||
dataIndex: 'lastTriggeredAt',
|
||||
key: 'lastTriggeredAt',
|
||||
width: 160,
|
||||
render: (date: string) =>
|
||||
date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-',
|
||||
},
|
||||
{
|
||||
title: intl.get('操作'),
|
||||
key: 'action',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
render: (_: any, record: any) => (
|
||||
<Dropdown menu={getActionsMenu(record)} trigger={['click']}>
|
||||
<Button type="link" icon={<EllipsisOutlined />} />
|
||||
</Dropdown>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
header={{
|
||||
title: intl.get('场景模式'),
|
||||
extra: [
|
||||
<Button
|
||||
key="create"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
{intl.get('新建场景')}
|
||||
</Button>,
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={scenarios}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: 1200 }}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `${intl.get('共')} ${total} ${intl.get('项')}`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScenarioModal
|
||||
visible={isModalVisible}
|
||||
scenario={selectedScenario}
|
||||
onCancel={() => {
|
||||
setIsModalVisible(false);
|
||||
setSelectedScenario(null);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setIsModalVisible(false);
|
||||
setSelectedScenario(null);
|
||||
fetchScenarios();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScenarioLogModal
|
||||
visible={isLogModalVisible}
|
||||
scenario={selectedScenario}
|
||||
onCancel={() => {
|
||||
setIsLogModalVisible(false);
|
||||
setSelectedScenario(null);
|
||||
}}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Scenario;
|
||||
130
src/pages/scenario/logModal.tsx
Normal file
130
src/pages/scenario/logModal.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Table, Tag, Typography } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import intl from 'react-intl-universal';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface ScenarioLogModalProps {
|
||||
visible: boolean;
|
||||
scenario: any;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const ScenarioLogModal: React.FC<ScenarioLogModalProps> = ({
|
||||
visible,
|
||||
scenario,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [logs, setLogs] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && scenario) {
|
||||
fetchLogs();
|
||||
}
|
||||
}, [visible, scenario]);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { code, data } = await request.get('/api/scenarios/logs', {
|
||||
params: {
|
||||
scenarioId: scenario.id,
|
||||
limit: 100,
|
||||
},
|
||||
});
|
||||
if (code === 200) {
|
||||
setLogs(data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch logs:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: intl.get('时间'),
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 180,
|
||||
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: intl.get('状态'),
|
||||
dataIndex: 'executionStatus',
|
||||
key: 'executionStatus',
|
||||
width: 100,
|
||||
render: (status: string) => {
|
||||
const colors: any = {
|
||||
success: 'success',
|
||||
failure: 'error',
|
||||
partial: 'warning',
|
||||
};
|
||||
return <Tag color={colors[status] || 'default'}>{status}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: intl.get('条件匹配'),
|
||||
dataIndex: 'conditionsMatched',
|
||||
key: 'conditionsMatched',
|
||||
width: 100,
|
||||
render: (matched: boolean) => (
|
||||
<Tag color={matched ? 'success' : 'default'}>
|
||||
{matched ? intl.get('是') : intl.get('否')}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: intl.get('执行时间'),
|
||||
dataIndex: 'executionTime',
|
||||
key: 'executionTime',
|
||||
width: 100,
|
||||
render: (time: number) => (time ? `${time}ms` : '-'),
|
||||
},
|
||||
{
|
||||
title: intl.get('重试次数'),
|
||||
dataIndex: 'retriesAttempted',
|
||||
key: 'retriesAttempted',
|
||||
width: 100,
|
||||
render: (retries: number) => retries || 0,
|
||||
},
|
||||
{
|
||||
title: intl.get('错误信息'),
|
||||
dataIndex: 'errorMessage',
|
||||
key: 'errorMessage',
|
||||
ellipsis: true,
|
||||
render: (error: string) => (
|
||||
<Text type={error ? 'danger' : undefined}>{error || '-'}</Text>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${intl.get('场景日志')} - ${scenario?.name || ''}`}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={1000}
|
||||
destroyOnClose
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={logs}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
showSizeChanger: true,
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScenarioLogModal;
|
||||
443
src/pages/scenario/modal.tsx
Normal file
443
src/pages/scenario/modal.tsx
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
InputNumber,
|
||||
Switch,
|
||||
Button,
|
||||
Space,
|
||||
Card,
|
||||
message,
|
||||
Divider,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { request } from '@/utils/http';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
interface ScenarioModalProps {
|
||||
visible: boolean;
|
||||
scenario: any;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const ScenarioModal: React.FC<ScenarioModalProps> = ({
|
||||
visible,
|
||||
scenario,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [triggerType, setTriggerType] = useState('time');
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (scenario) {
|
||||
form.setFieldsValue({
|
||||
...scenario,
|
||||
conditions: scenario.conditions || [],
|
||||
actions: scenario.actions || [],
|
||||
});
|
||||
setTriggerType(scenario.triggerType);
|
||||
} else {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
isEnabled: 1,
|
||||
conditionLogic: 'AND',
|
||||
failureThreshold: 3,
|
||||
delayExecution: 0,
|
||||
conditions: [],
|
||||
actions: [],
|
||||
});
|
||||
setTriggerType('time');
|
||||
}
|
||||
}
|
||||
}, [visible, scenario, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
const endpoint = scenario ? '/api/scenarios' : '/api/scenarios';
|
||||
const method = scenario ? 'put' : 'post';
|
||||
const payload = scenario ? { ...values, id: scenario.id } : values;
|
||||
|
||||
const { code } = await request[method](endpoint, payload);
|
||||
if (code === 200) {
|
||||
message.success(
|
||||
scenario ? intl.get('更新成功') : intl.get('创建成功'),
|
||||
);
|
||||
onSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save scenario:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderTriggerConfig = () => {
|
||||
switch (triggerType) {
|
||||
case 'variable':
|
||||
return (
|
||||
<Form.Item
|
||||
name={['triggerConfig', 'watchPath']}
|
||||
label={intl.get('监听路径')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input placeholder="/path/to/watch" />
|
||||
</Form.Item>
|
||||
);
|
||||
case 'webhook':
|
||||
return (
|
||||
<Form.Item name={['triggerConfig', 'token']} label="Token">
|
||||
<Input
|
||||
placeholder={intl.get('留空自动生成')}
|
||||
disabled={!!scenario}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
case 'time':
|
||||
return (
|
||||
<Form.Item
|
||||
name={['triggerConfig', 'schedule']}
|
||||
label={intl.get('Cron 表达式')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input placeholder="0 0 * * *" />
|
||||
</Form.Item>
|
||||
);
|
||||
case 'system_event':
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name={['triggerConfig', 'eventType']}
|
||||
label={intl.get('事件类型')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="disk_space">{intl.get('磁盘空间')}</Option>
|
||||
<Option value="memory">{intl.get('内存使用')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['triggerConfig', 'threshold']}
|
||||
label={intl.get('阈值')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber min={0} max={100} addonAfter="%" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['triggerConfig', 'checkInterval']}
|
||||
label={intl.get('检查间隔')}
|
||||
>
|
||||
<InputNumber min={10000} addonAfter="ms" />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
case 'task_status':
|
||||
return (
|
||||
<Form.Item
|
||||
name={['triggerConfig', 'cronId']}
|
||||
label={intl.get('任务 ID')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={scenario ? intl.get('编辑场景') : intl.get('新建场景')}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleSubmit}
|
||||
confirmLoading={loading}
|
||||
width={800}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={intl.get('名称')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label={intl.get('描述')}>
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="triggerType"
|
||||
label={intl.get('触发类型')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select onChange={setTriggerType}>
|
||||
<Option value="time">{intl.get('时间触发')}</Option>
|
||||
<Option value="variable">{intl.get('变量监听')}</Option>
|
||||
<Option value="webhook">Webhook</Option>
|
||||
<Option value="task_status">{intl.get('任务状态')}</Option>
|
||||
<Option value="system_event">{intl.get('系统事件')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{renderTriggerConfig()}
|
||||
|
||||
<Divider orientation="left">{intl.get('条件配置')}</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="conditionLogic"
|
||||
label={intl.get('条件逻辑')}
|
||||
tooltip={intl.get('多个条件之间的关系')}
|
||||
>
|
||||
<Select>
|
||||
<Option value="AND">AND ({intl.get('全部满足')})</Option>
|
||||
<Option value="OR">OR ({intl.get('任一满足')})</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.List name="conditions">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Card
|
||||
key={field.key}
|
||||
size="small"
|
||||
style={{ marginBottom: 8 }}
|
||||
extra={
|
||||
<DeleteOutlined
|
||||
onClick={() => remove(field.name)}
|
||||
style={{ color: 'red' }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Space>
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'field']}
|
||||
noStyle
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={intl.get('字段名')}
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'operator']}
|
||||
noStyle
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select placeholder={intl.get('操作符')} style={{ width: 120 }}>
|
||||
<Option value="equals">=</Option>
|
||||
<Option value="not_equals">!=</Option>
|
||||
<Option value="greater_than">></Option>
|
||||
<Option value="less_than"><</Option>
|
||||
<Option value="contains">{intl.get('包含')}</Option>
|
||||
<Option value="not_contains">{intl.get('不包含')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'value']}
|
||||
noStyle
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input
|
||||
placeholder={intl.get('值')}
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
{intl.get('添加条件')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
<Divider orientation="left">{intl.get('动作配置')}</Divider>
|
||||
|
||||
<Form.List name="actions">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Card
|
||||
key={field.key}
|
||||
size="small"
|
||||
style={{ marginBottom: 8 }}
|
||||
extra={
|
||||
<DeleteOutlined
|
||||
onClick={() => remove(field.name)}
|
||||
style={{ color: 'red' }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'type']}
|
||||
label={intl.get('动作类型')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="run_task">{intl.get('运行任务')}</Option>
|
||||
<Option value="set_variable">{intl.get('设置变量')}</Option>
|
||||
<Option value="execute_command">{intl.get('执行命令')}</Option>
|
||||
<Option value="send_notification">{intl.get('发送通知')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.actions?.[field.name]?.type !==
|
||||
currentValues.actions?.[field.name]?.type
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const actionType = getFieldValue([
|
||||
'actions',
|
||||
field.name,
|
||||
'type',
|
||||
]);
|
||||
if (actionType === 'run_task') {
|
||||
return (
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'cronId']}
|
||||
label={intl.get('任务 ID')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber min={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
if (actionType === 'set_variable') {
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'name']}
|
||||
label={intl.get('变量名')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'value']}
|
||||
label={intl.get('变量值')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (actionType === 'execute_command') {
|
||||
return (
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'command']}
|
||||
label={intl.get('命令')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
if (actionType === 'send_notification') {
|
||||
return (
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'message']}
|
||||
label={intl.get('消息')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</Card>
|
||||
))}
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
{intl.get('添加动作')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
<Divider orientation="left">{intl.get('高级设置')}</Divider>
|
||||
|
||||
<Form.Item name="delayExecution" label={intl.get('延迟执行')}>
|
||||
<InputNumber min={0} addonAfter={intl.get('秒')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="failureThreshold"
|
||||
label={intl.get('失败熔断阈值')}
|
||||
tooltip={intl.get('连续失败多少次后自动禁用')}
|
||||
>
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name={['retryStrategy', 'maxRetries']} label={intl.get('最大重试次数')}>
|
||||
<InputNumber min={0} max={10} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={['retryStrategy', 'retryDelay']}
|
||||
label={intl.get('重试延迟')}
|
||||
>
|
||||
<InputNumber min={1} addonAfter={intl.get('秒')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={['retryStrategy', 'backoffMultiplier']}
|
||||
label={intl.get('退避倍数')}
|
||||
tooltip={intl.get('每次重试延迟的乘数')}
|
||||
>
|
||||
<InputNumber min={1} step={0.5} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="isEnabled"
|
||||
label={intl.get('启用')}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScenarioModal;
|
||||
Loading…
Reference in New Issue
Block a user