diff --git a/frontend/src/components/AdvancedCronScheduler.tsx b/frontend/src/components/AdvancedCronScheduler.tsx new file mode 100644 index 0000000..8c331d3 --- /dev/null +++ b/frontend/src/components/AdvancedCronScheduler.tsx @@ -0,0 +1,556 @@ +import React, { useState, useCallback, useEffect } from "react"; +import { + Card, + Select, + Input, + Space, + Typography, + Row, + Col, + Divider, + Button, + Tooltip, +} from "antd"; +import { InfoCircleOutlined } from "@ant-design/icons"; + +const { Title, Text } = Typography; +const { Option } = Select; + +export interface AdvancedCronConfig { + second: string; + minute: string; + hour: string; + day: string; + month: string; + weekday: string; + year?: string; + cronExpression: string; +} + +interface AdvancedCronSchedulerProps { + value?: AdvancedCronConfig; + onChange?: (config: AdvancedCronConfig) => void; + showYear?: boolean; // 是否显示年份字段 + className?: string; +} + +// Cron字段的选项配置 +const CRON_OPTIONS = { + second: { + label: "秒", + range: [0, 59], + examples: ["0", "*/5", "10,20,30", "0-30"], + description: "秒钟 (0-59)", + }, + minute: { + label: "分钟", + range: [0, 59], + examples: ["0", "*/15", "5,10,15", "0-30"], + description: "分钟 (0-59)", + }, + hour: { + label: "小时", + range: [0, 23], + examples: ["0", "*/2", "8,14,20", "9-17"], + description: "小时 (0-23)", + }, + day: { + label: "日", + range: [1, 31], + examples: ["*", "1", "1,15", "1-15", "*/2"], + description: "日期 (1-31)", + }, + month: { + label: "月", + range: [1, 12], + examples: ["*", "1", "1,6,12", "3-9", "*/3"], + description: "月份 (1-12)", + }, + year: { + label: "年", + range: [1970, 2099], + examples: ["*", "2024", "2024-2026", "*/2"], + description: "年份 (1970-2099)", + }, + weekday: { + label: "周", + range: [0, 7], // 0和7都表示周日 + examples: ["*", "1", "1-5", "1,3,5", "0,6"], + description: "星期 (0-7, 0和7都表示周日)", + weekNames: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"], + }, +}; + +// 生成常用的cron表达式选项 +const generateCommonOptions = (field: keyof typeof CRON_OPTIONS) => { + const options = [ + { label: "* (任意)", value: "*" }, + { label: "? (不指定)", value: "?" }, // 仅用于日和周字段 + ]; + + const config = CRON_OPTIONS[field]; + const [start, end] = config.range; + + // 添加具体数值选项 + if (field === "weekday") { + const weekdayConfig = config as { weekNames?: string[] }; + weekdayConfig.weekNames?.forEach((name: string, index: number) => { + options.push({ label: `${index} (${name})`, value: index.toString() }); + }); + // 添加7作为周日的别名 + options.push({ label: "7 (周日)", value: "7" }); + } else { + // 添加部分具体数值 + const step = + field === "year" ? 5 : field === "day" || field === "month" ? 3 : 5; + for (let i = start; i <= end; i += step) { + if (i <= end) { + options.push({ label: i.toString(), value: i.toString() }); + } + } + } + + // 添加间隔选项 + if (field !== "year") { + options.push( + { label: "*/2 (每2个)", value: "*/2" }, + { label: "*/5 (每5个)", value: "*/5" }, + { label: "*/10 (每10个)", value: "*/10" } + ); + } + + // 添加范围选项 + if (field === "hour") { + options.push( + { label: "9-17 (工作时间)", value: "9-17" }, + { label: "0-6 (凌晨)", value: "0-6" } + ); + } else if (field === "weekday") { + options.push( + { label: "1-5 (工作日)", value: "1-5" }, + { label: "0,6 (周末)", value: "0,6" } + ); + } else if (field === "day") { + options.push( + { label: "1-15 (上半月)", value: "1-15" }, + { label: "16-31 (下半月)", value: "16-31" } + ); + } + + return options; +}; + +// 验证cron字段值 +const validateCronField = ( + value: string, + field: keyof typeof CRON_OPTIONS +): boolean => { + if (!value || value === "*" || value === "?") return true; + + const config = CRON_OPTIONS[field]; + const [min, max] = config.range; + + // 验证基本格式 + const patterns = [ + /^\d+$/, // 单个数字 + /^\d+-\d+$/, // 范围 + /^\*\/\d+$/, // 间隔 + /^(\d+,)*\d+$/, // 列表 + /^(\d+-\d+,)*(\d+-\d+|\d+)$/, // 复合表达式 + ]; + + if (!patterns.some((pattern) => pattern.test(value))) { + return false; + } + + // 验证数值范围 + const numbers = value.match(/\d+/g); + if (numbers) { + return numbers.every((num) => { + const n = parseInt(num); + return n >= min && n <= max; + }); + } + + return true; +}; + +// 生成cron表达式 +const generateCronExpression = ( + config: Omit +): string => { + const { second, minute, hour, day, month, weekday, year } = config; + + const parts = [second, minute, hour, day, month, weekday]; + if (year && year !== "*") { + parts.push(year); + } + + return parts.join(" "); +}; + +// 解析cron表达式为人类可读的描述 +const parseCronDescription = (cronExpression: string): string => { + const parts = cronExpression.split(" "); + if (parts.length < 6) return cronExpression; + + const [second, minute, hour, day, month, weekday, year] = parts; + + const descriptions = []; + + // 时间描述 + if (second === "0" && minute !== "*" && hour !== "*") { + descriptions.push(`${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`); + } else { + if (hour !== "*") descriptions.push(`${hour}时`); + if (minute !== "*") descriptions.push(`${minute}分`); + if (second !== "*" && second !== "0") descriptions.push(`${second}秒`); + } + + // 日期描述 + if (day !== "*" && day !== "?") { + descriptions.push(`${day}日`); + } + + // 月份描述 + if (month !== "*") { + descriptions.push(`${month}月`); + } + + // 星期描述 + if (weekday !== "*" && weekday !== "?") { + const weekNames = [ + "周日", + "周一", + "周二", + "周三", + "周四", + "周五", + "周六", + "周日", + ]; + if (weekday === "1-5") { + descriptions.push("工作日"); + } else if (weekday === "0,6") { + descriptions.push("周末"); + } else if (/^\d$/.test(weekday)) { + descriptions.push(weekNames[parseInt(weekday)]); + } else { + descriptions.push(`周${weekday}`); + } + } + + // 年份描述 + if (year && year !== "*") { + descriptions.push(`${year}年`); + } + + return descriptions.length > 0 ? descriptions.join(" ") : "每秒执行"; +}; + +const AdvancedCronScheduler: React.FC = ({ + value = { + second: "0", + minute: "0", + hour: "0", + day: "*", + month: "*", + weekday: "?", + year: "*", + cronExpression: "0 0 0 * * ?", + }, + onChange, + showYear = false, + className, +}) => { + const [config, setConfig] = useState(value); + const [customMode, setCustomMode] = useState(false); + + // 更新配置 + const updateConfig = useCallback( + (updates: Partial) => { + const newConfig = { ...config, ...updates }; + newConfig.cronExpression = generateCronExpression(newConfig); + setConfig(newConfig); + onChange?.(newConfig); + }, + [config, onChange] + ); + + // 同步外部值 + useEffect(() => { + setConfig(value); + }, [value]); + + // 处理字段变化 + const handleFieldChange = ( + field: keyof AdvancedCronConfig, + fieldValue: string + ) => { + if (field === "cronExpression") { + // 直接编辑cron表达式 + const parts = fieldValue.split(" "); + if (parts.length >= 6) { + const newConfig = { + second: parts[0] || "0", + minute: parts[1] || "0", + hour: parts[2] || "0", + day: parts[3] || "*", + month: parts[4] || "*", + weekday: parts[5] || "?", + year: parts[6] || "*", + cronExpression: fieldValue, + }; + setConfig(newConfig); + onChange?.(newConfig); + } + } else { + updateConfig({ [field]: fieldValue }); + } + }; + + // 快速设置预设 + const setPreset = (preset: Partial) => { + updateConfig(preset); + }; + + // 常用预设 + const commonPresets = [ + { + label: "每秒", + config: { + second: "*", + minute: "*", + hour: "*", + day: "*", + month: "*", + weekday: "?", + }, + }, + { + label: "每分钟", + config: { + second: "0", + minute: "*", + hour: "*", + day: "*", + month: "*", + weekday: "?", + }, + }, + { + label: "每小时", + config: { + second: "0", + minute: "0", + hour: "*", + day: "*", + month: "*", + weekday: "?", + }, + }, + { + label: "每天午夜", + config: { + second: "0", + minute: "0", + hour: "0", + day: "*", + month: "*", + weekday: "?", + }, + }, + { + label: "每周一9点", + config: { + second: "0", + minute: "0", + hour: "9", + day: "?", + month: "*", + weekday: "1", + }, + }, + { + label: "每月1日0点", + config: { + second: "0", + minute: "0", + hour: "0", + day: "1", + month: "*", + weekday: "?", + }, + }, + { + label: "工作日9点", + config: { + second: "0", + minute: "0", + hour: "9", + day: "?", + month: "*", + weekday: "1-5", + }, + }, + { + label: "每15分钟", + config: { + second: "0", + minute: "*/15", + hour: "*", + day: "*", + month: "*", + weekday: "?", + }, + }, + ]; + + const fields: Array = [ + "second", + "minute", + "hour", + "day", + "month", + "weekday", + ]; + if (showYear) fields.push("year"); + + return ( + + + {/* 标题和切换模式 */} +
+ + 高级 Cron 表达式配置 + + +
+ + {/* 快速预设 */} +
+ 快速预设: +
+ {commonPresets.map((preset, index) => ( + + ))} +
+
+ + {customMode ? ( + /* 手动编辑模式 */ +
+ Cron 表达式: + + handleFieldChange("cronExpression", e.target.value) + } + placeholder="秒 分 时 日 月 周 [年]" + /> + + 格式:秒(0-59) 分(0-59) 时(0-23) 日(1-31) 月(1-12) 周(0-7) + [年(1970-2099)] + +
+ ) : ( + /* 向导模式 */ + + {fields.map((field) => { + const fieldConfig = CRON_OPTIONS[field]; + const options = generateCommonOptions(field); + return ( + +
+
+ {fieldConfig.label} + + + +
+ + {/* 自定义输入 */} + handleFieldChange(field, e.target.value)} + status={ + validateCronField(config[field] || "", field) ? "" : "error" + } + /> +
+ + ); + })} +
+ )} + + + + {/* 结果预览 */} +
+ 生成的 Cron 表达式: + + + 描述:{parseCronDescription(config.cronExpression)} + +
+ + {/* 字段说明 */} +
+ 字段说明: +
+ + • * 表示任意值 + • ? 表示不指定值(仅日、周字段) + • */5 表示每5个单位 + • 1-5 表示范围 + • 1,3,5 表示列表 + • 日和周字段不能同时指定具体值 + +
+
+
+
+ ); +}; + +export default AdvancedCronScheduler; diff --git a/frontend/src/components/CardView.tsx b/frontend/src/components/CardView.tsx index d3d8dc3..36b3a2d 100644 --- a/frontend/src/components/CardView.tsx +++ b/frontend/src/components/CardView.tsx @@ -1,11 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; -import { - Tag, - Pagination, - Tooltip, - Empty, - Popover, -} from "antd"; +import { Tag, Pagination, Tooltip, Empty, Popover, Spin } from "antd"; import { ClockCircleOutlined, StarFilled } from "@ant-design/icons"; import type { ItemType } from "antd/es/menu/interface"; import { formatDateTime } from "@/utils/unit"; diff --git a/frontend/src/mock/mock-seed/data-collection.cjs b/frontend/src/mock/mock-seed/data-collection.cjs index 5d0797a..8998b0b 100644 --- a/frontend/src/mock/mock-seed/data-collection.cjs +++ b/frontend/src/mock/mock-seed/data-collection.cjs @@ -3,37 +3,24 @@ const API = require("../mock-apis.cjs"); const { Random } = Mock; // 生成模拟数据归集统计信息 -const collectionStatistics = { - period: Random.pick(["HOUR", "DAY", "WEEK", "MONTH"]), - totalTasks: Random.integer(50, 200), - activeTasks: Random.integer(10, 50), - successfulExecutions: Random.integer(30, 150), - failedExecutions: Random.integer(0, 50), - totalExecutions: Random.integer(20, 100), - avgExecutionTime: Random.integer(1000, 10000), // in milliseconds - avgThroughput: Random.integer(100, 1000), // records per second - topDataSources: new Array(5).fill(null).map(() => ({ - dataSourceId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), - dataSourceName: Mock.Random.word(5, 15), - type: Mock.Random.pick([ - "MySQL", - "PostgreSQL", - "ORACLE", - "SQLSERVER", - "MONGODB", - "REDIS", - "ELASTICSEARCH", - "HIVE", - "HDFS", - "KAFKA", - "HTTP", - "FILE", - ]), - taskCount: Mock.Random.integer(1, 20), - executionCount: Mock.Random.integer(1, 50), - recordsProcessed: Mock.Random.integer(70, 100), // percentage - })), -}; +function dataXTemplate() { + return { + id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""), + name: Mock.Random.ctitle(5, 15), + sourceType: Mock.Random.csentence(3, 10), + targetType: Mock.Random.csentence(3, 10), + description: Mock.Random.csentence(5, 20), + version: `v${Mock.Random.integer(1, 5)}.${Mock.Random.integer( + 0, + 9 + )}.${Mock.Random.integer(0, 9)}`, + isSystem: Mock.Random.boolean(), + createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"), + updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"), + }; +} + +const templateList = new Array(20).fill(null).map(dataXTemplate); // 生成模拟任务数据 function taskItem() { @@ -89,30 +76,21 @@ function executionLogItem() { const executionLogList = new Array(100).fill(null).map(executionLogItem); module.exports = function (router) { - // 获取数据统计信息 - router.get(API.queryCollectionStatisticsUsingGet, (req, res) => { - res.send({ - code: "0", - msg: "Success", - data: collectionStatistics, - }); - }); - // 获取任务列表 - router.post(API.queryTasksUsingPost, (req, res) => { - const { searchTerm, filters, page = 1, size = 10 } = req.body; + router.get(API.queryTasksUsingGet, (req, res) => { + const { keyword, status, page = 0, size = 10 } = req.query; let filteredTasks = taskList; - if (searchTerm) { + if (keyword) { filteredTasks = filteredTasks.filter((task) => - task.name.includes(searchTerm) + task.name.includes(keyword) ); } - if (filters && filters.status && filters.status.length > 0) { + if (status && status.length > 0) { filteredTasks = filteredTasks.filter((task) => - filters.status.includes(task.status) + status.includes(task.status) ); } - const startIndex = (page - 1) * size; + const startIndex = page * size; const endIndex = startIndex + size; const paginatedTasks = filteredTasks.slice(startIndex, endIndex); @@ -123,7 +101,30 @@ module.exports = function (router) { totalElements: filteredTasks.length, page, size, - results: paginatedTasks, + content: paginatedTasks, + }, + }); + }); + + router.get(API.queryDataXTemplatesUsingGet, (req, res) => { + const { keyword, page = 0, size = 10 } = req.query; + let filteredTemplates = templateList; + if (keyword) { + filteredTemplates = filteredTemplates.filter((template) => + template.name.includes(keyword) + ); + } + const startIndex = page * size; + const endIndex = startIndex + size; + const paginatedTemplates = filteredTemplates.slice(startIndex, endIndex); + res.send({ + code: "0", + msg: "Success", + data: { + content: paginatedTemplates, + totalElements: filteredTemplates.length, + page, + size, }, }); }); diff --git a/frontend/src/pages/DataCollection/Create/CreateTask.tsx b/frontend/src/pages/DataCollection/Create/CreateTask.tsx index 2a1135b..506c445 100644 --- a/frontend/src/pages/DataCollection/Create/CreateTask.tsx +++ b/frontend/src/pages/DataCollection/Create/CreateTask.tsx @@ -1,20 +1,9 @@ import { useState } from "react"; -import { - Card, - Input, - Button, - Select, - Radio, - Form, - Divider, - InputNumber, - TimePicker, - App, -} from "antd"; +import { Input, Button, Radio, Form, InputNumber, App, Select } from "antd"; import { Link, useNavigate } from "react-router"; import { ArrowLeft } from "lucide-react"; import { createTaskUsingPost } from "../collection.apis"; -import DevelopmentInProgress from "@/components/DevelopmentInProgress"; +import SimpleCronScheduler from "@/pages/DataCollection/Create/SimpleCronScheduler"; const { TextArea } = Input; @@ -30,16 +19,16 @@ interface ScheduleConfig { const defaultTemplates = [ { - id: "nas-to-local", + id: "nas", name: "NAS到本地", description: "从NAS文件系统导入数据到本地文件系统", config: { - reader: "nasreader", + reader: "nfsreader", writer: "localwriter", }, }, { - id: "obs-to-local", + id: "obs", name: "OBS到本地", description: "从OBS文件系统导入数据到本地文件系统", config: { @@ -48,7 +37,7 @@ const defaultTemplates = [ }, }, { - id: "web-tolocal", + id: "web", name: "Web到本地", description: "从Web URL导入数据到本地文件系统", config: { @@ -58,9 +47,13 @@ const defaultTemplates = [ }, ]; -export default function CollectionTaskCreate() { - return ; +enum TemplateType { + NAS = "nas", + OBS = "obs", + WEB = "web", +} +export default function CollectionTaskCreate() { const navigate = useNavigate(); const [form] = Form.useForm(); const { message } = App.useApp(); @@ -68,7 +61,7 @@ export default function CollectionTaskCreate() { const [templateType, setTemplateType] = useState<"default" | "custom">( "default" ); - const [selectedTemplate, setSelectedTemplate] = useState(""); + const [selectedTemplate, setSelectedTemplate] = useState("nas"); const [customConfig, setCustomConfig] = useState(""); const [scheduleConfig, setScheduleConfig] = useState({ @@ -104,7 +97,7 @@ export default function CollectionTaskCreate() { }; return ( -
+
@@ -116,244 +109,234 @@ export default function CollectionTaskCreate() {
- -
{ - // 文件格式变化时重置模板选择 - if (_.fileFormat !== undefined) setSelectedTemplate(""); - }} - > - {/* 基本信息 */} -

基本信息

- - +
+ { + // 文件格式变化时重置模板选择 + if (_.fileFormat !== undefined) setSelectedTemplate(""); + }} > - - - -