You've already forked DataMate
data collection page (#31)
* feat: Update site name to DataMate and refine text for AI data processing * feat: Refactor settings page and implement model access functionality - Created a new ModelAccess component for managing model configurations. - Removed the old Settings component and replaced it with a new SettingsPage component that integrates ModelAccess, SystemConfig, and WebhookConfig. - Added SystemConfig component for managing system settings. - Implemented WebhookConfig component for managing webhook configurations. - Updated API functions for model management in settings.apis.ts. - Adjusted routing to point to the new SettingsPage component. * feat: Implement Data Collection Page with Task Management and Execution Log - Created DataCollectionPage component to manage data collection tasks. - Added TaskManagement and ExecutionLog components for task handling and logging. - Integrated task operations including start, stop, edit, and delete functionalities. - Implemented filtering and searching capabilities in task management. - Introduced SimpleCronScheduler for scheduling tasks with cron expressions. - Updated CreateTask component to utilize new scheduling and template features. - Enhanced BasicInformation component to conditionally render fields based on visibility settings. - Refactored ImportConfiguration component to remove NAS import section.
This commit is contained in:
556
frontend/src/components/AdvancedCronScheduler.tsx
Normal file
556
frontend/src/components/AdvancedCronScheduler.tsx
Normal file
@@ -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<AdvancedCronConfig, "cronExpression">
|
||||
): 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<AdvancedCronSchedulerProps> = ({
|
||||
value = {
|
||||
second: "0",
|
||||
minute: "0",
|
||||
hour: "0",
|
||||
day: "*",
|
||||
month: "*",
|
||||
weekday: "?",
|
||||
year: "*",
|
||||
cronExpression: "0 0 0 * * ?",
|
||||
},
|
||||
onChange,
|
||||
showYear = false,
|
||||
className,
|
||||
}) => {
|
||||
const [config, setConfig] = useState<AdvancedCronConfig>(value);
|
||||
const [customMode, setCustomMode] = useState(false);
|
||||
|
||||
// 更新配置
|
||||
const updateConfig = useCallback(
|
||||
(updates: Partial<AdvancedCronConfig>) => {
|
||||
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<AdvancedCronConfig>) => {
|
||||
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<keyof typeof CRON_OPTIONS> = [
|
||||
"second",
|
||||
"minute",
|
||||
"hour",
|
||||
"day",
|
||||
"month",
|
||||
"weekday",
|
||||
];
|
||||
if (showYear) fields.push("year");
|
||||
|
||||
return (
|
||||
<Card className={className} size="small">
|
||||
<Space direction="vertical" className="w-full" size="middle">
|
||||
{/* 标题和切换模式 */}
|
||||
<div className="flex justify-between items-center">
|
||||
<Title level={5} className="m-0">
|
||||
高级 Cron 表达式配置
|
||||
</Title>
|
||||
<Button
|
||||
size="small"
|
||||
type={customMode ? "primary" : "default"}
|
||||
onClick={() => setCustomMode(!customMode)}
|
||||
>
|
||||
{customMode ? "切换到向导模式" : "切换到手动模式"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 快速预设 */}
|
||||
<div>
|
||||
<Text strong>快速预设:</Text>
|
||||
<div className="mt-2 flex gap-2 flex-wrap">
|
||||
{commonPresets.map((preset, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
size="small"
|
||||
onClick={() => setPreset(preset.config)}
|
||||
>
|
||||
{preset.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{customMode ? (
|
||||
/* 手动编辑模式 */
|
||||
<div>
|
||||
<Text strong>Cron 表达式:</Text>
|
||||
<Input
|
||||
className="mt-2"
|
||||
value={config.cronExpression}
|
||||
onChange={(e) =>
|
||||
handleFieldChange("cronExpression", e.target.value)
|
||||
}
|
||||
placeholder="秒 分 时 日 月 周 [年]"
|
||||
/>
|
||||
<Text
|
||||
type="secondary"
|
||||
className="text-xs block mt-1"
|
||||
>
|
||||
格式:秒(0-59) 分(0-59) 时(0-23) 日(1-31) 月(1-12) 周(0-7)
|
||||
[年(1970-2099)]
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
/* 向导模式 */
|
||||
<Row gutter={[16, 16]}>
|
||||
{fields.map((field) => {
|
||||
const fieldConfig = CRON_OPTIONS[field];
|
||||
const options = generateCommonOptions(field);
|
||||
return (
|
||||
<Col xs={24} sm={12} md={8} key={field}>
|
||||
<div>
|
||||
<div className="flex items-center mb-1">
|
||||
<Text strong>{fieldConfig.label}</Text>
|
||||
<Tooltip title={fieldConfig.description}>
|
||||
<InfoCircleOutlined className="ml-1 text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Select
|
||||
className="w-full"
|
||||
value={config[field]}
|
||||
onChange={(val) => handleFieldChange(field, val)}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder={`选择${fieldConfig.label}`}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
{/* 自定义输入 */}
|
||||
<Input
|
||||
className="mt-1"
|
||||
size="small"
|
||||
placeholder="或输入自定义值"
|
||||
value={config[field] || ""}
|
||||
onChange={(e) => handleFieldChange(field, e.target.value)}
|
||||
status={
|
||||
validateCronField(config[field] || "", field) ? "" : "error"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Divider className="my-3" />
|
||||
|
||||
{/* 结果预览 */}
|
||||
<div>
|
||||
<Text strong>生成的 Cron 表达式:</Text>
|
||||
<Input
|
||||
className="mt-2 bg-gray-100"
|
||||
value={config.cronExpression}
|
||||
readOnly
|
||||
/>
|
||||
<Text
|
||||
type="secondary"
|
||||
className="text-xs block mt-1"
|
||||
>
|
||||
描述:{parseCronDescription(config.cronExpression)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 字段说明 */}
|
||||
<div>
|
||||
<Text strong>字段说明:</Text>
|
||||
<div className="mt-2 text-xs text-gray-600">
|
||||
<Row gutter={[16, 8]}>
|
||||
<Col span={12}>• * 表示任意值</Col>
|
||||
<Col span={12}>• ? 表示不指定值(仅日、周字段)</Col>
|
||||
<Col span={12}>• */5 表示每5个单位</Col>
|
||||
<Col span={12}>• 1-5 表示范围</Col>
|
||||
<Col span={12}>• 1,3,5 表示列表</Col>
|
||||
<Col span={12}>• 日和周字段不能同时指定具体值</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedCronScheduler;
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 <DevelopmentInProgress showTime="2025.10.30" />;
|
||||
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<ScheduleConfig>({
|
||||
@@ -104,7 +97,7 @@ export default function CollectionTaskCreate() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center">
|
||||
<Link to="/data/collection">
|
||||
@@ -116,27 +109,19 @@ export default function CollectionTaskCreate() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div className="flex-overflow-auto border-card">
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
name: "",
|
||||
datasetName: "",
|
||||
fileFormat: "",
|
||||
description: "",
|
||||
cronExpression: "",
|
||||
retryCount: 3,
|
||||
timeout: 3600,
|
||||
incrementalField: "",
|
||||
}}
|
||||
initialValues={scheduleConfig}
|
||||
onValuesChange={(_, allValues) => {
|
||||
// 文件格式变化时重置模板选择
|
||||
if (_.fileFormat !== undefined) setSelectedTemplate("");
|
||||
}}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<h2 className="font-medium text-gray-900 text-lg mb-4">基本信息</h2>
|
||||
<h2 className="font-medium text-gray-900 text-lg mb-2">基本信息</h2>
|
||||
|
||||
<Form.Item
|
||||
label="任务名称"
|
||||
@@ -148,12 +133,11 @@ export default function CollectionTaskCreate() {
|
||||
<Form.Item label="描述" name="description">
|
||||
<TextArea placeholder="请输入任务描述" rows={3} />
|
||||
</Form.Item>
|
||||
<Form.Item label="文件格式" name="fileFormat">
|
||||
<Input placeholder="请填写文件格式,使用正则表达式" />
|
||||
</Form.Item>
|
||||
|
||||
{/* 同步配置 */}
|
||||
<h2 className="font-medium text-gray-900 my-4 text-lg">同步配置</h2>
|
||||
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-lg">
|
||||
同步配置
|
||||
</h2>
|
||||
<Form.Item label="同步方式">
|
||||
<Radio.Group
|
||||
value={scheduleConfig.type}
|
||||
@@ -168,104 +152,30 @@ export default function CollectionTaskCreate() {
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{scheduleConfig.type === "scheduled" && (
|
||||
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Form.Item label="调度类型">
|
||||
<Select
|
||||
options={[
|
||||
{ label: "每日", value: "day" },
|
||||
{ label: "每周", value: "week" },
|
||||
{ label: "每月", value: "month" },
|
||||
{ label: "自定义Cron", value: "custom" },
|
||||
]}
|
||||
value={scheduleConfig.scheduleType}
|
||||
onChange={(value) =>
|
||||
setScheduleConfig((prev) => ({
|
||||
...prev,
|
||||
scheduleType: value as ScheduleConfig["scheduleType"],
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
{scheduleConfig.scheduleType === "custom" ? (
|
||||
<Form.Item
|
||||
label="Cron表达式"
|
||||
label=""
|
||||
name="cronExpression"
|
||||
rules={[{ required: true, message: "请输入Cron表达式" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="例如:0 0 * * * 表示每天午夜执行"
|
||||
value={scheduleConfig.cronExpression}
|
||||
onChange={(e) =>
|
||||
setScheduleConfig((prev) => ({
|
||||
...prev,
|
||||
cronExpression: e.target.value,
|
||||
}))
|
||||
<SimpleCronScheduler
|
||||
className="px-2 rounded"
|
||||
value={scheduleConfig.cronExpression || "* * * * *"}
|
||||
showYear
|
||||
onChange={(value) =>
|
||||
setScheduleConfig({ ...scheduleConfig, cron: value })
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item label="执行时间" className="w-full">
|
||||
{scheduleConfig.scheduleType === "day" ? (
|
||||
<TimePicker />
|
||||
) : (
|
||||
<Select
|
||||
options={
|
||||
scheduleConfig.scheduleType === "week"
|
||||
? [
|
||||
{ label: "周一", value: "1" },
|
||||
{ label: "周二", value: "2" },
|
||||
{ label: "周三", value: "3" },
|
||||
{ label: "周四", value: "4" },
|
||||
{ label: "周五", value: "5" },
|
||||
{ label: "周六", value: "6" },
|
||||
{ label: "周日", value: "0" },
|
||||
]
|
||||
: [
|
||||
{ label: "每月1日", value: "1" },
|
||||
{ label: "每月5日", value: "5" },
|
||||
{ label: "每月10日", value: "10" },
|
||||
{ label: "每月15日", value: "15" },
|
||||
{ label: "每月20日", value: "20" },
|
||||
{ label: "每月25日", value: "25" },
|
||||
{ label: "每月30日", value: "30" },
|
||||
]
|
||||
}
|
||||
placeholder={
|
||||
scheduleConfig.scheduleType === "week"
|
||||
? "选择星期几"
|
||||
: "选择日期"
|
||||
}
|
||||
value={scheduleConfig.dayOfWeek}
|
||||
onChange={(value) =>
|
||||
setScheduleConfig((prev) => ({
|
||||
...prev,
|
||||
dayOfWeek: value as string,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Form.Item label="最大执行次数">
|
||||
<InputNumber
|
||||
min={1}
|
||||
value={scheduleConfig.maxRetries}
|
||||
onChange={(value) =>
|
||||
setScheduleConfig((prev) => ({
|
||||
...prev,
|
||||
maxRetries: value,
|
||||
}))
|
||||
}
|
||||
className="w-full"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
<Form.Item name="maxRetries" label="最大执行次数">
|
||||
<InputNumber min={1} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
|
||||
{/* 模板配置 */}
|
||||
<h2 className="font-medium text-gray-900 my-4 text-lg">模板配置</h2>
|
||||
<Form.Item label="模板类型">
|
||||
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-lg">
|
||||
模板配置
|
||||
</h2>
|
||||
{/* <Form.Item label="模板类型">
|
||||
<Radio.Group
|
||||
value={templateType}
|
||||
onChange={(e) => setTemplateType(e.target.value)}
|
||||
@@ -273,9 +183,10 @@ export default function CollectionTaskCreate() {
|
||||
<Radio value="default">使用默认模板</Radio>
|
||||
<Radio value="custom">自定义DataX JSON配置</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Form.Item> */}
|
||||
{templateType === "default" && (
|
||||
<Form.Item label="选择模板">
|
||||
<>
|
||||
{/* <Form.Item label="选择模板">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{defaultTemplates.map((template) => (
|
||||
<div
|
||||
@@ -288,14 +199,88 @@ export default function CollectionTaskCreate() {
|
||||
onClick={() => setSelectedTemplate(template.id)}
|
||||
>
|
||||
<div className="font-medium">{template.name}</div>
|
||||
<div className="text-gray-500">{template.description}</div>
|
||||
<div className="text-gray-500">
|
||||
{template.description}
|
||||
</div>
|
||||
<div className="text-gray-400">
|
||||
{template.config.reader} → {template.config.writer}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Form.Item> */}
|
||||
{/* nas import */}
|
||||
{selectedTemplate === TemplateType.NAS && (
|
||||
<div className="grid grid-cols-2 gap-3 px-2 rounded">
|
||||
<Form.Item
|
||||
name="nasPath"
|
||||
rules={[{ required: true, message: "请输入NAS地址" }]}
|
||||
label="NAS地址"
|
||||
>
|
||||
<Input placeholder="192.168.1.100" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="sharePath"
|
||||
rules={[{ required: true, message: "请输入共享路径" }]}
|
||||
label="共享路径"
|
||||
>
|
||||
<Input placeholder="/share/importConfig" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="fileList"
|
||||
label="文件列表"
|
||||
className="col-span-2"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择文件列表"
|
||||
mode="tags"
|
||||
multiple
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* obs import */}
|
||||
{selectedTemplate === TemplateType.OBS && (
|
||||
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
|
||||
<Form.Item
|
||||
name="endpoint"
|
||||
rules={[{ required: true }]}
|
||||
label="Endpoint"
|
||||
>
|
||||
<Input
|
||||
className="h-8 text-xs"
|
||||
placeholder="obs.cn-north-4.myhuaweicloud.com"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bucket"
|
||||
rules={[{ required: true }]}
|
||||
label="Bucket"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="my-bucket" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="accessKey"
|
||||
rules={[{ required: true }]}
|
||||
label="Access Key"
|
||||
>
|
||||
<Input className="h-8 text-xs" placeholder="Access Key" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="secretKey"
|
||||
rules={[{ required: true }]}
|
||||
label="Secret Key"
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
className="h-8 text-xs"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{templateType === "custom" && (
|
||||
@@ -343,17 +328,15 @@ export default function CollectionTaskCreate() {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<Divider />
|
||||
<div className="flex gap-2 justify-end">
|
||||
</Form>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end border-top p-6">
|
||||
<Button onClick={() => navigate("/data/collection")}>取消</Button>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
207
frontend/src/pages/DataCollection/Create/SimpleCronScheduler.tsx
Normal file
207
frontend/src/pages/DataCollection/Create/SimpleCronScheduler.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import {
|
||||
Card,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
TimePicker,
|
||||
Button,
|
||||
Input,
|
||||
Form,
|
||||
} from "antd";
|
||||
import type { RadioChangeEvent } from "antd";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
export interface SimpleCronConfig {
|
||||
type: "once" | "daily" | "weekly" | "monthly";
|
||||
time?: string; // HH:mm 格式
|
||||
weekDay?: number; // 0-6, 0 表示周日
|
||||
monthDay?: number; // 1-31
|
||||
cronExpression: string;
|
||||
}
|
||||
|
||||
interface SimpleCronSchedulerProps {
|
||||
value?: SimpleCronConfig;
|
||||
onChange?: (config: SimpleCronConfig) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultConfig: SimpleCronConfig = {
|
||||
type: "once",
|
||||
time: "00:00",
|
||||
cronExpression: "0 0 0 * * ?",
|
||||
};
|
||||
|
||||
// 生成周几选项
|
||||
const weekDayOptions = [
|
||||
{ label: "周日", value: 0 },
|
||||
{ label: "周一", value: 1 },
|
||||
{ label: "周二", value: 2 },
|
||||
{ label: "周三", value: 3 },
|
||||
{ label: "周四", value: 4 },
|
||||
{ label: "周五", value: 5 },
|
||||
{ label: "周六", value: 6 },
|
||||
];
|
||||
|
||||
// 生成月份日期选项
|
||||
const monthDayOptions = Array.from({ length: 31 }, (_, i) => ({
|
||||
label: `${i + 1}日`,
|
||||
value: i + 1,
|
||||
}));
|
||||
|
||||
// 常用时间预设
|
||||
const commonTimePresets = [
|
||||
{ label: "上午 9:00", value: "09:00" },
|
||||
{ label: "中午 12:00", value: "12:00" },
|
||||
{ label: "下午 2:00", value: "14:00" },
|
||||
{ label: "下午 6:00", value: "18:00" },
|
||||
{ label: "晚上 8:00", value: "20:00" },
|
||||
{ label: "午夜 0:00", value: "00:00" },
|
||||
];
|
||||
|
||||
const SimpleCronScheduler: React.FC<SimpleCronSchedulerProps> = ({
|
||||
value = defaultConfig,
|
||||
onChange,
|
||||
className,
|
||||
}) => {
|
||||
const [config, setConfig] = useState<SimpleCronConfig>(value);
|
||||
|
||||
// 更新配置并生成 cron 表达式
|
||||
const updateConfig = useCallback(
|
||||
(updates: Partial<SimpleCronConfig>) => {
|
||||
const newConfig = { ...config, ...updates };
|
||||
const [hour, minute] = (newConfig.time || "00:00").split(":");
|
||||
|
||||
// 根据不同类型生成 cron 表达式
|
||||
let cronExpression = "";
|
||||
switch (newConfig.type) {
|
||||
case "once":
|
||||
cronExpression = `0 ${minute} ${hour} * * ?`;
|
||||
break;
|
||||
case "daily":
|
||||
cronExpression = `0 ${minute} ${hour} * * ?`;
|
||||
break;
|
||||
case "weekly":
|
||||
cronExpression = `0 ${minute} ${hour} ? * ${newConfig.weekDay}`;
|
||||
break;
|
||||
case "monthly":
|
||||
cronExpression = `0 ${minute} ${hour} ${newConfig.monthDay} * ?`;
|
||||
break;
|
||||
}
|
||||
|
||||
newConfig.cronExpression = cronExpression;
|
||||
setConfig(newConfig);
|
||||
onChange?.(newConfig);
|
||||
},
|
||||
[config, onChange]
|
||||
);
|
||||
|
||||
// 处理类型改变
|
||||
const handleTypeChange = (type) => {
|
||||
const updates: Partial<SimpleCronConfig> = { type };
|
||||
|
||||
// 设置默认值
|
||||
if (type === "weekly" && !config.weekDay) {
|
||||
updates.weekDay = 1; // 默认周一
|
||||
} else if (type === "monthly" && !config.monthDay) {
|
||||
updates.monthDay = 1; // 默认每月1号
|
||||
}
|
||||
|
||||
updateConfig(updates);
|
||||
};
|
||||
|
||||
// 处理时间改变
|
||||
const handleTimeChange = (value: Dayjs | null) => {
|
||||
if (value) {
|
||||
updateConfig({ time: value.format("HH:mm") });
|
||||
}
|
||||
};
|
||||
|
||||
// 快速设置预设时间
|
||||
const handleTimePreset = (time: string) => {
|
||||
updateConfig({ time });
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className={`w-full ${className || ""}`}>
|
||||
{/* 执行周期选择 */}
|
||||
<Form.Item label="执行周期" required>
|
||||
<Select value={config.type} onChange={handleTypeChange}>
|
||||
<Select.Option value="once">仅执行一次</Select.Option>
|
||||
<Select.Option value="daily">每天执行</Select.Option>
|
||||
<Select.Option value="weekly">每周执行</Select.Option>
|
||||
<Select.Option value="monthly">每月执行</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* 时间选择 */}
|
||||
<Form.Item label="执行时间" required>
|
||||
<Space wrap>
|
||||
<TimePicker
|
||||
format="HH:mm"
|
||||
value={config.time ? dayjs(config.time, "HH:mm") : null}
|
||||
onChange={handleTimeChange}
|
||||
placeholder="选择时间"
|
||||
/>
|
||||
<Space wrap className="mt-2">
|
||||
{commonTimePresets.map((preset) => (
|
||||
<Button
|
||||
key={preset.value}
|
||||
size="small"
|
||||
className={
|
||||
config.time === preset.value ? "ant-btn-primary" : ""
|
||||
}
|
||||
onClick={() => handleTimePreset(preset.value)}
|
||||
>
|
||||
{preset.label}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
{/* 周几选择 */}
|
||||
{config.type === "weekly" && (
|
||||
<Form.Item label="执行日期" required>
|
||||
<Select
|
||||
className="w-32"
|
||||
value={config.weekDay}
|
||||
onChange={(weekDay) => updateConfig({ weekDay })}
|
||||
placeholder="选择周几"
|
||||
options={weekDayOptions}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 月份日期选择 */}
|
||||
{config.type === "monthly" && (
|
||||
<Form.Item label="执行日期" required>
|
||||
<Select
|
||||
className="w-32"
|
||||
value={config.monthDay}
|
||||
onChange={(monthDay) => updateConfig({ monthDay })}
|
||||
placeholder="选择日期"
|
||||
options={monthDayOptions}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* Cron 表达式预览 */}
|
||||
{/* <div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<Text>生成的 Cron 表达式</Text>
|
||||
<Input
|
||||
className="mt-2 bg-gray-100"
|
||||
value={config.cronExpression}
|
||||
readOnly
|
||||
/>
|
||||
</div> */}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleCronScheduler;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Tabs } from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import TaskManagement from "./components/TaskManagement";
|
||||
import ExecutionLog from "./components/ExecutionLog";
|
||||
import TaskManagement from "./TaskManagement";
|
||||
import ExecutionLog from "./ExecutionLog";
|
||||
import { useNavigate } from "react-router";
|
||||
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
|
||||
|
||||
@@ -10,10 +10,10 @@ export default function DataCollection() {
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("task-management");
|
||||
|
||||
return <DevelopmentInProgress showTime="2025.10.30" />;
|
||||
// return <DevelopmentInProgress showTime="2025.10.30" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="gap-4 h-full flex flex-col">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900 mb-2">数据归集</h1>
|
||||
@@ -2,8 +2,8 @@ import { Card, Badge, Table } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import type { CollectionLog } from "@/pages/DataCollection/collection.model";
|
||||
import { queryExecutionLogUsingPost } from "../../collection.apis";
|
||||
import { LogStatusMap, LogTriggerTypeMap } from "../../collection.const";
|
||||
import { queryExecutionLogUsingPost } from "../collection.apis";
|
||||
import { LogStatusMap, LogTriggerTypeMap } from "../collection.const";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
|
||||
const filterOptions = [
|
||||
@@ -1,16 +1,34 @@
|
||||
import { Card, Button, Badge, Table, Dropdown, App } from "antd";
|
||||
import { EllipsisOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Badge,
|
||||
Table,
|
||||
Dropdown,
|
||||
App,
|
||||
Tooltip,
|
||||
Popconfirm,
|
||||
} from "antd";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EllipsisOutlined,
|
||||
PauseCircleOutlined,
|
||||
PauseOutlined,
|
||||
PlayCircleOutlined,
|
||||
StopOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import {
|
||||
deleteTaskByIdUsingDelete,
|
||||
executeTaskByIdUsingPost,
|
||||
queryTasksUsingGet,
|
||||
stopTaskByIdUsingPost,
|
||||
} from "../../collection.apis";
|
||||
import { TaskStatus, type CollectionTask } from "../../collection.model";
|
||||
import { StatusMap, SyncModeMap } from "../../collection.const";
|
||||
} from "../collection.apis";
|
||||
import { TaskStatus, type CollectionTask } from "../collection.model";
|
||||
import { StatusMap, SyncModeMap } from "../collection.const";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import { useNavigate } from "react-router";
|
||||
import { mapCollectionTask } from "../collection.const";
|
||||
|
||||
export default function TaskManagement() {
|
||||
const { message } = App.useApp();
|
||||
@@ -34,7 +52,7 @@ export default function TaskManagement() {
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
} = useFetchData(queryTasksUsingGet);
|
||||
} = useFetchData(queryTasksUsingGet, mapCollectionTask);
|
||||
|
||||
const handleStartTask = async (taskId: string) => {
|
||||
await executeTaskByIdUsingPost(taskId);
|
||||
@@ -54,34 +72,60 @@ export default function TaskManagement() {
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const taskOperations = (record: CollectionTask) => {
|
||||
const isStopped = record.status === TaskStatus.STOPPED;
|
||||
const startButton = {
|
||||
key: "start",
|
||||
label: "启动",
|
||||
icon: <PlayCircleOutlined />,
|
||||
onClick: () => handleStartTask(record.id),
|
||||
};
|
||||
const stopButton = {
|
||||
key: "stop",
|
||||
label: "停止",
|
||||
icon: <PauseCircleOutlined />,
|
||||
onClick: () => handleStopTask(record.id),
|
||||
};
|
||||
const items = [
|
||||
isStopped ? startButton : stopButton,
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => {
|
||||
showEditTaskModal(record);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
confirm: {
|
||||
title: "确定要删除该任务吗?此操作不可撤销。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okType: "danger",
|
||||
},
|
||||
onClick: () => handleDeleteTask(record.id),
|
||||
},
|
||||
];
|
||||
return items;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
render: (text: string, record: CollectionTask) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate("`/data-collection/tasks/${record.id}`)}>")}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (status: string) =>
|
||||
StatusMap[status] ? (
|
||||
<Badge
|
||||
color={StatusMap[status].color}
|
||||
text={StatusMap[status].label}
|
||||
/>
|
||||
) : (
|
||||
<Badge text={status} />
|
||||
render: (status: string) => (
|
||||
<Badge text={status.label} color={status.color} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -115,47 +159,42 @@ export default function TaskManagement() {
|
||||
title: "操作",
|
||||
key: "action",
|
||||
fixed: "right" as const,
|
||||
render: (_: any, record: Task) => (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
record.status === TaskStatus.STOPPED
|
||||
? {
|
||||
key: "start",
|
||||
label: "启动",
|
||||
onClick: () => handleStartTask(record.id),
|
||||
}
|
||||
: {
|
||||
key: "stop",
|
||||
label: "停止",
|
||||
onClick: () => handleStopTask(record.id),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
onClick: () => handleViewDetail(record),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
onClick: () => handleDeleteTask(record.id),
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
render: (_: any, record: CollectionTask) => {
|
||||
return taskOperations(record).map((op) => {
|
||||
const button = (
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EllipsisOutlined style={{ fontSize: 20 }} />}
|
||||
icon={op.icon}
|
||||
danger={op?.danger}
|
||||
onClick={() => op.onClick(record)}
|
||||
/>
|
||||
</Dropdown>
|
||||
),
|
||||
</Tooltip>
|
||||
);
|
||||
if (op.confirm) {
|
||||
return (
|
||||
<Popconfirm
|
||||
key={op.key}
|
||||
title={op.confirm.title}
|
||||
okText={op.confirm.okText}
|
||||
cancelText={op.confirm.cancelText}
|
||||
okType={op.danger ? "danger" : "primary"}
|
||||
onConfirm={() => op.onClick(record)}
|
||||
>
|
||||
<Tooltip key={op.key} title={op.label}>
|
||||
<Button type="text" icon={op.icon} danger={op?.danger} />
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
return button;
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-4">
|
||||
{/* Header Actions */}
|
||||
<SearchControls
|
||||
searchTerm={searchParams.keyword}
|
||||
@@ -176,7 +215,6 @@ export default function TaskManagement() {
|
||||
filters: {},
|
||||
}))
|
||||
}
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
{/* Tasks Table */}
|
||||
@@ -192,7 +230,7 @@ export default function TaskManagement() {
|
||||
pageSize: searchParams.pageSize,
|
||||
total: pagination.total,
|
||||
}}
|
||||
scroll={{ x: "max-content" }}
|
||||
scroll={{ x: "max-content", y: "calc(100vh - 25rem)" }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -1,4 +1,9 @@
|
||||
import { LogStatus, SyncMode, TaskStatus, TriggerType } from "./collection.model";
|
||||
import {
|
||||
LogStatus,
|
||||
SyncMode,
|
||||
TaskStatus,
|
||||
TriggerType,
|
||||
} from "./collection.model";
|
||||
|
||||
export const StatusMap: Record<
|
||||
TaskStatus,
|
||||
@@ -67,3 +72,10 @@ export const LogTriggerTypeMap: Record<
|
||||
[TriggerType.SCHEDULED]: { label: "定时", value: TriggerType.SCHEDULED },
|
||||
[TriggerType.API]: { label: "API", value: TriggerType.API },
|
||||
};
|
||||
|
||||
export function mapCollectionTask(task: CollectionTask): CollectionTask {
|
||||
return {
|
||||
...task,
|
||||
status: StatusMap[task.status],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function BasicInformation({
|
||||
|
||||
// 获取标签
|
||||
const fetchTags = async () => {
|
||||
if (hidden.includes("tags")) return;
|
||||
try {
|
||||
const { data } = await queryDatasetTagsUsingGet();
|
||||
const customTags = data.map((tag) => ({
|
||||
@@ -47,9 +48,11 @@ export default function BasicInformation({
|
||||
>
|
||||
<Input placeholder="输入数据集名称" />
|
||||
</Form.Item>
|
||||
{!hidden.includes("description") && (
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea placeholder="描述数据集的用途和内容" rows={3} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 数据集类型选择 - 使用卡片形式 */}
|
||||
{!hidden.includes("datasetType") && (
|
||||
@@ -65,7 +68,8 @@ export default function BasicInformation({
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="tags" label="标签">
|
||||
{!hidden.includes("tags") && (
|
||||
<Form.Item label="标签" name="tags">
|
||||
<Select
|
||||
className="w-full"
|
||||
mode="tags"
|
||||
@@ -73,6 +77,7 @@ export default function BasicInformation({
|
||||
placeholder="请选择标签"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,40 +152,6 @@ export default function ImportConfiguration({
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* nas import */}
|
||||
{importConfig?.source === DataSource.NAS && (
|
||||
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
|
||||
<Form.Item
|
||||
name="nasPath"
|
||||
rules={[{ required: true }]}
|
||||
label="NAS地址"
|
||||
>
|
||||
<Input placeholder="192.168.1.100" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="sharePath"
|
||||
rules={[{ required: true }]}
|
||||
label="共享路径"
|
||||
>
|
||||
<Input placeholder="/share/importConfig" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true }]}
|
||||
label="用户名"
|
||||
>
|
||||
<Input placeholder="用户名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true }]}
|
||||
label="密码"
|
||||
>
|
||||
<Input type="password" placeholder="密码" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* obs import */}
|
||||
{importConfig?.source === DataSource.OBS && (
|
||||
<div className="grid grid-cols-2 gap-3 p-4 bg-blue-50 rounded-lg">
|
||||
|
||||
Reference in New Issue
Block a user