diff --git a/deployment/helm/datamate/values.yaml b/deployment/helm/datamate/values.yaml index 78570f4fb..c14306ad9 100644 --- a/deployment/helm/datamate/values.yaml +++ b/deployment/helm/datamate/values.yaml @@ -337,6 +337,10 @@ ray-cluster: key: DB_PASSWORD - name: PG_DATABASE value: "datamate" + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name resources: limits: cpu: "8" @@ -352,7 +356,7 @@ ray-cluster: volumeMounts: - mountPath: /tmp/ray name: log-volume - subPath: ray/worker + subPath: ray/$(POD_NAME) - mountPath: /dataset name: dataset-volume - mountPath: /flow diff --git a/frontend/public/config/error-code.json b/frontend/public/config/error-code.json index 6eb1c30a4..d5dd78969 100644 --- a/frontend/public/config/error-code.json +++ b/frontend/public/config/error-code.json @@ -14,8 +14,10 @@ "cleaning.0009": "设置解析错误", "cleaning.0010": "任务ID不能为空", "cleaning.0011": "无法删除预制模板", + "cleaning.0012": "清洗任务日志文件不存在", + "cleaning.0013": "任务状态无效,无法执行此操作", "operator.0001": "算子不存在", - "operator.0002": "算子被编排于模版中或处在正在进行的任务中,无法删除", + "operator.0002": "算子被编排于模版中或处在尚未结束的任务中,无法删除", "operator.0003": "无法删除预置算子", "operator.0004": "不支持的文件类型,当前仅支持tar和zip", "operator.0005": "解析算子包失败", @@ -49,4 +51,4 @@ "404": "请求的资源不存在", "500": "服务器内部错误,请稍后重试", "502": "网关错误" -} \ No newline at end of file +} diff --git a/frontend/src/components/ActionDropdown.tsx b/frontend/src/components/ActionDropdown.tsx index 6d7d6ba4d..87cbc6482 100644 --- a/frontend/src/components/ActionDropdown.tsx +++ b/frontend/src/components/ActionDropdown.tsx @@ -112,7 +112,7 @@ const ActionDropdown = ({ <> dropdownContent} + popupRender={() => dropdownContent} trigger={["click"]} placement={placement} open={open} diff --git a/frontend/src/components/DetailHeader.tsx b/frontend/src/components/DetailHeader.tsx index cbd332619..c918944a8 100644 --- a/frontend/src/components/DetailHeader.tsx +++ b/frontend/src/components/DetailHeader.tsx @@ -271,8 +271,8 @@ function DetailHeader({

{(data as any)?.description}

- {statistics.map((stat: any) => ( -
+ {statistics.map((stat: StatisticItem, index: number) => ( +
{stat.icon} {stat.value}
@@ -281,10 +281,11 @@ function DetailHeader({
- {operations.map((op: any) => { + {operations.map((op: OperationItem) => { if (op.isDropdown) { return ( diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 3fe17885d..bbbc00ad0 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -1458,6 +1458,9 @@ "deleteDesc": "Are you sure you want to delete \"{{itemName}}\"? This action cannot be undone." } }, + "create": { + "typeMismatch": "Type mismatch: previous operator outputs \"{{from}}\", but current operator requires \"{{to}}\"" + }, "template": { "columns": { "templateName": "Template Name", @@ -1594,6 +1597,7 @@ }, "status": { "completed": "Completed", + "partialSuccess": "Partial Success", "failed": "Failed", "running": "Running", "pending": "Pending", @@ -2600,6 +2604,7 @@ "unknown": "Unknown", "processing": "Processing", "completed": "Completed", + "partialSuccess": "Partial Success", "failed": "Failed" }, "operations": { diff --git a/frontend/src/i18n/locales/zh/common.json b/frontend/src/i18n/locales/zh/common.json index d16f37ccd..ce3283139 100644 --- a/frontend/src/i18n/locales/zh/common.json +++ b/frontend/src/i18n/locales/zh/common.json @@ -1458,6 +1458,9 @@ "deleteDesc": "删除「{{itemName}}」后将无法恢复,确定要执行此操作吗?" } }, + "create": { + "typeMismatch": "类型不兼容:上一个算子输出类型为「{{from}}」,当前算子输入类型为「{{to}}」" + }, "template": { "columns": { "templateName": "模板名称", @@ -1526,7 +1529,7 @@ "currentDisplay": "当前展示: 第 {{num}} 次", "nthRun": "第 {{num}} 次", "noLogs": "当前任务无可用日志", - "streaming": "实时流式输出中...", + "streaming": "实时输出中...", "download": "下载日志", "downloadFailed": "下载日志文件失败" }, @@ -1594,6 +1597,7 @@ }, "status": { "completed": "已完成", + "partialSuccess": "部分成功", "failed": "失败", "running": "运行中", "pending": "等待中", diff --git a/frontend/src/pages/DataCleansing/Create/hooks/useOperatorOperations.ts b/frontend/src/pages/DataCleansing/Create/hooks/useOperatorOperations.ts index 25e67db22..fb9ccd58f 100644 --- a/frontend/src/pages/DataCleansing/Create/hooks/useOperatorOperations.ts +++ b/frontend/src/pages/DataCleansing/Create/hooks/useOperatorOperations.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { OperatorI } from "@/pages/OperatorMarket/operator.model"; import { CleansingTemplate } from "../../cleansing.model"; import {queryCleaningTemplateByIdUsingGet, queryCleaningTemplatesUsingGet} from "../../cleansing.api"; @@ -8,9 +8,22 @@ import { } from "@/pages/OperatorMarket/operator.api"; import {useParams} from "react-router"; import i18n from "@/i18n"; +import { App } from "antd"; + +function checkTypeCompatible( + prevOutputs: string | undefined, + nextInputs: string | undefined +): boolean { + if (!prevOutputs || !nextInputs) return true; + const outputs = prevOutputs.toLowerCase().trim(); + const inputs = nextInputs.toLowerCase().trim(); + if (outputs === "multimodal" || inputs === "multimodal") return true; + return outputs === inputs; +} export function useOperatorOperations() { const { id = "" } = useParams(); + const { message } = App.useApp(); const [currentStep, setCurrentStep] = useState(1); const [operators, setOperators] = useState([]); @@ -115,6 +128,19 @@ export function useOperatorOperations() { selectedOperators.filter((op) => op.id !== operator.id) ); } else { + // Validate type compatibility with the last selected operator + if (selectedOperators.length > 0) { + const lastOp = selectedOperators[selectedOperators.length - 1]; + if (!checkTypeCompatible(lastOp.outputs, operator.inputs)) { + message.warning( + i18n.t("dataCleansing.create.typeMismatch", { + from: lastOp.outputs, + to: operator.inputs, + }) + ); + return; + } + } setSelectedOperators([...selectedOperators, { ...operator }]); } }; diff --git a/frontend/src/pages/DataCleansing/Detail/TaskDetail.tsx b/frontend/src/pages/DataCleansing/Detail/TaskDetail.tsx index 0d5203efc..bae3c10f9 100644 --- a/frontend/src/pages/DataCleansing/Detail/TaskDetail.tsx +++ b/frontend/src/pages/DataCleansing/Detail/TaskDetail.tsx @@ -119,24 +119,22 @@ export default function CleansingTaskDetail() { { icon: , label: t("dataCleansing.detail.statistics.successFiles"), - value: task?.progress?.succeedFileNum || "0", + value: task?.progress?.succeedFileNum || 0, }, { icon: , label: t("dataCleansing.detail.statistics.failedFiles"), - value: (task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ? - task?.progress.failedFileNum : - task?.progress?.totalFileNum - task?.progress.succeedFileNum, + value: task?.progress?.failedFileNum || 0, }, { icon: , label: t("dataCleansing.detail.statistics.successRate"), - value: task?.progress?.successRate ? task?.progress?.successRate + "%" : "--", + value: task?.progress?.process ? task?.progress?.process + "%" : "--", }, ]; const operations = [ - ...(task?.status === TaskStatus.RUNNING + ...(task?.status?.value === TaskStatus.RUNNING ? [ { key: "pause", @@ -146,11 +144,11 @@ export default function CleansingTaskDetail() { }, ] : []), - ...([TaskStatus.PENDING, TaskStatus.STOPPED, TaskStatus.FAILED].includes(task?.status?.value) + ...([TaskStatus.PENDING, TaskStatus.STOPPED, TaskStatus.FAILED, TaskStatus.PARTIAL_SUCCESS].includes(task?.status?.value) ? [ { key: "start", - label: t("dataCleansing.actions.updateTask"), + label: t("dataCleansing.actions.start"), icon: , onClick: startTask, }, @@ -217,7 +215,7 @@ export default function CleansingTaskDetail() { )} {activeTab === "operators" && } {activeTab === "files" && } - {activeTab === "logs" && } + {activeTab === "logs" && }
diff --git a/frontend/src/pages/DataCleansing/Detail/components/BasicInfo.tsx b/frontend/src/pages/DataCleansing/Detail/components/BasicInfo.tsx index 65d5f08a1..0ea0e643b 100644 --- a/frontend/src/pages/DataCleansing/Detail/components/BasicInfo.tsx +++ b/frontend/src/pages/DataCleansing/Detail/components/BasicInfo.tsx @@ -83,7 +83,7 @@ export default function BasicInfo({ task }: { task: CleansingTask }) {
- {task?.progress?.succeedFileNum || "0"} + {task?.progress?.succeedFileNum ?? 0}
{t("dataCleansing.detail.statistics.successFiles")}
@@ -91,8 +91,8 @@ export default function BasicInfo({ task }: { task: CleansingTask }) {
{(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ? - task?.progress.failedFileNum : - task?.progress?.totalFileNum - task?.progress.succeedFileNum} + (task?.progress?.failedFileNum ?? 0) : + Math.max(0, (task?.progress?.totalFileNum ?? 0) - (task?.progress?.succeedFileNum ?? 0))}
{t("dataCleansing.detail.statistics.failedFiles")}
@@ -113,33 +113,35 @@ export default function BasicInfo({ task }: { task: CleansingTask }) { column={2} bordered={false} size="middle" - labelStyle={{ fontWeight: 500, color: "#555" }} - contentStyle={{ fontSize: 14 }} + styles={{ label: { fontWeight: 500, color: "#555" }, content: { fontSize: 14 } }} items={descriptionItems} > {/* 处理进度 */}

{t("dataCleansing.detail.basicInfo.processingProgress")}

- { task?.status?.value === TaskStatus.FAILED ? + { task?.status?.value === TaskStatus.FAILED ? ( - : - } + ) : task?.status?.value === TaskStatus.PARTIAL_SUCCESS ? ( + + ) : ( + + )}
- {t("dataCleansing.detail.basicInfo.completed", { count: task?.progress?.succeedFileNum || "0" })} + {t("dataCleansing.detail.basicInfo.completed", { count: task?.progress?.succeedFileNum ?? 0 })}
{t("dataCleansing.detail.basicInfo.processing", { count: (task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ? - task?.progress?.totalFileNum - task?.progress.succeedFileNum : 0 })} + Math.max(0, (task?.progress?.totalFileNum ?? 0) - (task?.progress?.succeedFileNum ?? 0)) : 0 })}
{t("dataCleansing.detail.basicInfo.failed", { count: (task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ? - task?.progress.failedFileNum : - task?.progress?.totalFileNum - task?.progress.succeedFileNum })} + (task?.progress?.failedFileNum ?? 0) : + Math.max(0, (task?.progress?.totalFileNum ?? 0) - (task?.progress?.succeedFileNum ?? 0)) })}
diff --git a/frontend/src/pages/DataCleansing/Detail/components/FileTable.tsx b/frontend/src/pages/DataCleansing/Detail/components/FileTable.tsx index cb48c0948..a577a61d4 100644 --- a/frontend/src/pages/DataCleansing/Detail/components/FileTable.tsx +++ b/frontend/src/pages/DataCleansing/Detail/components/FileTable.tsx @@ -6,6 +6,72 @@ import {TaskStatus} from "@/pages/DataCleansing/cleansing.model.ts"; import {getTaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx"; import { useTranslation } from "react-i18next"; +// 渲染 JSON 值的辅助组件 +function JsonValue({ value, depth = 0 }: { value: any; depth?: number }) { + if (value === null) { + return null; + } + if (value === undefined) { + return undefined; + } + if (typeof value === 'boolean') { + return {value.toString()}; + } + if (typeof value === 'number') { + return {value}; + } + if (typeof value === 'string') { + // 处理换行符,保留空白格式 + const lines = value.split('\n'); + if (lines.length > 1) { + return ( + + {lines.map((line, i) => ( + + {line} + {i < lines.length - 1 &&
} +
+ ))} +
+ ); + } + return {value}; + } + if (Array.isArray(value)) { + if (value.length === 0) { + return []; + } + return ( +
+ {value.map((item, i) => ( +
+ [{i}] + +
+ ))} +
+ ); + } + if (typeof value === 'object') { + const keys = Object.keys(value); + if (keys.length === 0) { + return {"{}"}; + } + return ( +
+ {keys.map((key) => ( +
+ {key} + : + +
+ ))} +
+ ); + } + return {String(value)}; +} + // 模拟文件列表数据 export default function FileTable({result, fetchTaskResult}) { const { id = "" } = useParams(); @@ -20,7 +86,7 @@ export default function FileTable({result, fetchTaskResult}) { const handleSelectAllFiles = (checked: boolean) => { if (checked) { - setSelectedFileIds(result.map((file) => file.instanceId)); + setSelectedFileIds(result.map((file) => file.srcFileId)); } else { setSelectedFileIds([]); } @@ -87,8 +153,8 @@ export default function FileTable({result, fetchTaskResult}) { render: (_text: string, record: any) => ( handleSelectFile(record.id, e.target.checked)} + checked={selectedFileIds.includes(record.srcFileId)} + onChange={(e) => handleSelectFile(record.srcFileId, e.target.checked)} className="w-4 h-4" /> ), @@ -283,48 +349,85 @@ export default function FileTable({result, fetchTaskResult}) { title: t("dataCleansing.detail.fileTable.result"), dataIndex: "result", key: "result", - width: 200, - render: (text: string) => { - if (!text) return -; + width: 120, + render: (text: string, record: any) => { + // 如果结果为空或特殊值,则不展示 + if (!text || text === '' || text === '{}' || text === '[]' || + text === 'null' || text === 'undefined' || text.trim() === '') { + return -; + } + // 尝试解析JSON try { const parsed = JSON.parse(text); - const jsonString = JSON.stringify(parsed, null, 2); - const displayText = typeof parsed === 'object' - ? (Array.isArray(parsed) ? `[${parsed.length} items]` : '{...}') - : String(parsed); + // 如果是空对象或空数组则不展示 + if (Array.isArray(parsed) && parsed.length === 0) { + return -; + } + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) && Object.keys(parsed).length === 0) { + return -; + } + + // 检查 reason 字段:如果只有 reason 且为 null/空,则不展示 + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + + const keys = Object.keys(parsed); + // 如果只有 reason 字段且为空 + if (keys.length > 0 && keys.includes('reason')) { + const reason = parsed['reason']; + if (reason === null || reason === undefined || reason === '' || + (typeof reason === 'string' && reason.trim() === '')) { + return -; + } + } + } + + // 有内容需要展示,显示 {...} 点击查看详情 return ( - {jsonString} - +
+ +
} title={t("dataCleansing.detail.fileTable.resultDetail")} trigger="click" > - - {displayText} + + {"{...}"}
); } catch { - const displayText = text.length > 30 ? text.substring(0, 30) + '...' : text; + // 普通字符串:如果为空则不展示 + if (text.trim() === '') { + return -; + } + // 有内容的普通字符串,点击查看详情(支持换行) + const lines = text.split('\n'); return ( {text}} + content={ +
+ {lines.map((line, i) => ( + + {line} + {i < lines.length - 1 &&
} +
+ ))} +
+ } title={t("dataCleansing.detail.fileTable.resultDetail")} trigger="click" - disabled={text.length <= 30} > - - {displayText} + + {"{...}"}
); } - }, + } }, { title: t("dataCleansing.detail.fileTable.status"), @@ -403,7 +506,7 @@ export default function FileTable({result, fetchTaskResult}) { dataSource={result} pagination={{ pageSize: 10, showSizeChanger: true }} size="middle" - rowKey="id" + rowKey={(record) => record.srcFileId || record.instanceId} /> {/* 文件对比弹窗 */} diff --git a/frontend/src/pages/DataCleansing/Detail/components/LogsTable.tsx b/frontend/src/pages/DataCleansing/Detail/components/LogsTable.tsx index b474f412d..7080803bb 100644 --- a/frontend/src/pages/DataCleansing/Detail/components/LogsTable.tsx +++ b/frontend/src/pages/DataCleansing/Detail/components/LogsTable.tsx @@ -1,40 +1,54 @@ -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState, useRef, useMemo } from "react"; import { useParams } from "react-router"; import { FileClock, Download } from "lucide-react"; import { useTranslation } from "react-i18next"; import { App } from "antd"; +import VirtualList from 'rc-virtual-list'; import { streamCleaningTaskLog, downloadCleaningTaskLog } from "../../cleansing.api"; +import { TaskStatus } from "../../cleansing.model"; interface LogEntry { level: string; message: string; } -export default function LogsTable({ taskLog: initialLogs, fetchTaskLog, retryCount, taskName }: { taskLog: LogEntry[], fetchTaskLog: () => Promise, retryCount: number, taskName: string }) { +interface LogEntryWithIndex extends LogEntry { + index: number; +} + +export default function LogsTable({ + taskLog: initialLogs, + fetchTaskLog, + retryCount, + taskName, + taskStatus +}: { + taskLog: LogEntry[], + fetchTaskLog: () => Promise, + retryCount: number, + taskName: string, + taskStatus?: TaskStatus +}) { const { id = "" } = useParams(); const { t } = useTranslation(); const [selectedLog, setSelectedLog] = useState(retryCount + 1); const [streamingLogs, setStreamingLogs] = useState([]); const [isStreaming, setIsStreaming] = useState(false); - const logContainerRef = useRef(null); const eventSourceRef = useRef(null); const { message } = App.useApp(); + // Only stream when task is RUNNING and viewing the latest run + const shouldStream = taskStatus === TaskStatus.RUNNING && selectedLog - 1 === retryCount; + useEffect(() => { - if (selectedLog - 1 === retryCount) { + if (shouldStream) { startStreaming(); } else { stopStreaming(); fetchTaskLog(selectedLog - 1); } return () => stopStreaming(); - }, [id, selectedLog, retryCount]); - - useEffect(() => { - if (logContainerRef.current && isStreaming) { - logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; - } - }, [streamingLogs, isStreaming]); + }, [id, selectedLog, retryCount, shouldStream]); const startStreaming = () => { stopStreaming(); @@ -73,7 +87,20 @@ export default function LogsTable({ taskLog: initialLogs, fetchTaskLog, retryCou setIsStreaming(false); }; - const displayLogs = selectedLog - 1 === retryCount ? streamingLogs : initialLogs; + // Use streaming logs only when actively streaming, otherwise use initial logs + const displayLogs = isStreaming ? streamingLogs : initialLogs; + + // Add index to logs for virtual list key + const logsWithIndex: LogEntryWithIndex[] = useMemo(() => { + return (displayLogs || []).map((log, index) => ({ ...log, index })); + }, [displayLogs]); + + // Get log level color class + const getLevelClass = (level: string) => { + if (level === "ERROR" || level === "FATAL") return "text-red-500"; + if (level === "WARNING" || level === "WARN") return "text-yellow-500"; + return "text-green-500"; + }; const handleSelectChange = (value: number) => { setSelectedLog(value); @@ -130,29 +157,24 @@ export default function LogsTable({ taskLog: initialLogs, fetchTaskLog, retryCou
-
- {displayLogs?.map?.((log, index) => ( -
- - [{log.level}] - - {log.message} -
- ))} +
+ + {(log: LogEntryWithIndex) => ( +
+ + [{log.level}] + + {log.message} +
+ )} +
{isStreaming && ( -
- [INFO] +
...
)} diff --git a/frontend/src/pages/DataCleansing/Detail/components/OperatorTable.tsx b/frontend/src/pages/DataCleansing/Detail/components/OperatorTable.tsx index d03887c9a..c7d127bbc 100644 --- a/frontend/src/pages/DataCleansing/Detail/components/OperatorTable.tsx +++ b/frontend/src/pages/DataCleansing/Detail/components/OperatorTable.tsx @@ -3,6 +3,7 @@ import {useNavigate} from "react-router"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/Card" import { GitBranch } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { TaskStatus } from "@/pages/DataCleansing/cleansing.model"; export default function OperatorTable({ task }: { task: any }) { const navigate = useNavigate(); @@ -83,23 +84,27 @@ export default function OperatorTable({ task }: { task: any }) { dataIndex: "status", key: "status", filters: [ - { text: t("dataCleansing.detail.operatorTable.completed"), value: t("dataCleansing.detail.operatorTable.completed") }, - { text: t("dataCleansing.detail.operatorTable.failed"), value: t("dataCleansing.detail.operatorTable.failed") }, - { text: t("dataCleansing.detail.operatorTable.running"), value: t("dataCleansing.detail.operatorTable.running") }, + { text: t("dataCleansing.detail.operatorTable.completed"), value: "completed" }, + { text: t("dataCleansing.detail.operatorTable.failed"), value: "failed" }, + { text: t("dataCleansing.detail.operatorTable.running"), value: "running" }, + { text: t("dataCleansing.detail.operatorTable.partialSuccess"), value: "partialSuccess" }, ], - onFilter: (value: string, record: any) => record.status === value, - render: (status: string) => ( - - ), + onFilter: (value: string, record: any) => record.statusValue === value, + render: (statusObj: { label: string; value: TaskStatus }) => { + let badgeStatus: "default" | "processing" | "success" | "error" | "warning" = "default"; + + if (statusObj?.value === TaskStatus.COMPLETED) { + badgeStatus = "success"; + } else if (statusObj?.value === TaskStatus.RUNNING) { + badgeStatus = "processing"; + } else if (statusObj?.value === TaskStatus.PARTIAL_SUCCESS) { + badgeStatus = "warning"; + } else if (statusObj?.value === TaskStatus.FAILED) { + badgeStatus = "error"; + } + + return ; + }, }, ] @@ -114,18 +119,25 @@ export default function OperatorTable({ task }: { task: any }) { {t("dataCleansing.detail.operatorTable.description")} - ({ - id: item?.id, - name: item?.name, - startTime: new Date(task?.startedAt).toLocaleTimeString(), - endTime: task?.finishedAt - ? new Date(task.finishedAt).toLocaleTimeString() - : '-', - duration: task.duration, - status: task.status.label, - processedFiles: task.progress.finishedFileNum, - successRate: task?.progress.successRate, - }))} pagination={false} size="middle" /> +
({ + id: item?.id, + name: item?.name, + startTime: new Date(task?.startedAt).toLocaleTimeString(), + endTime: task?.finishedAt + ? new Date(task.finishedAt).toLocaleTimeString() + : '-', + duration: task.duration, + status: task.status, + statusValue: task.status.value, + processedFiles: task.progress.finishedFileNum, + successRate: task?.progress.successRate, + }))} + rowKey="id" + pagination={false} + size="middle" + /> diff --git a/frontend/src/pages/DataCleansing/Home/components/TaskList.tsx b/frontend/src/pages/DataCleansing/Home/components/TaskList.tsx index b7895b2ee..d1d26cce3 100644 --- a/frontend/src/pages/DataCleansing/Home/components/TaskList.tsx +++ b/frontend/src/pages/DataCleansing/Home/components/TaskList.tsx @@ -93,46 +93,41 @@ export default function TaskList() { setDeleteModal({ visible: false, taskId: "", taskName: "" }); }; - useEffect(() => { - fetchData(); - }, [t]); - const taskOperations = (record: CleansingTask) => { const isRunning = record.status?.value === TaskStatus.RUNNING; const showStart = [ TaskStatus.PENDING, + TaskStatus.PARTIAL_SUCCESS, TaskStatus.FAILED, TaskStatus.STOPPED, ].includes(record.status?.value); - const isComplete = record.status?.value === TaskStatus.COMPLETED; const pauseBtn = { key: "pause", label: t("dataCleansing.actions.pause"), - icon: isRunning ? : , - onClick: pauseTask, // implement pause/play logic + icon: , + onClick: pauseTask, }; const startBtn = { key: "start", label: t("dataCleansing.actions.start"), - icon: isRunning ? : , - disabled: isComplete, - onClick: startTask, // implement pause/play logic + icon: , + onClick: startTask, + }; + + const deleteBtn = { + key: "delete", + label: t("dataCleansing.actions.delete"), + icon: , + danger: true, + disabled: isRunning, // 运行中的任务禁用删除按钮 + onClick: deleteTask, }; + return [ - ...(isRunning - ? [ pauseBtn ] - : []), - ...(showStart || isComplete - ? [ startBtn ] - : []), - { - key: "delete", - label: t("dataCleansing.actions.delete"), - danger: true, - icon: , - onClick: deleteTask, // implement delete logic - }, + ...(isRunning ? [pauseBtn] : []), + ...(showStart ? [startBtn] : []), + deleteBtn, ]; }; @@ -217,9 +212,12 @@ export default function TaskList() { key: "process", width: 150, render: (_, record: CleansingTask) => { - if (record?.status?.value == TaskStatus.FAILED) { + if (record?.status?.value === TaskStatus.FAILED) { return ; } + if (record?.status?.value === TaskStatus.PARTIAL_SUCCESS) { + return ; + } return ; }, }, diff --git a/frontend/src/pages/DataCleansing/cleansing.const.tsx b/frontend/src/pages/DataCleansing/cleansing.const.tsx index 5a27a3523..111503cdb 100644 --- a/frontend/src/pages/DataCleansing/cleansing.const.tsx +++ b/frontend/src/pages/DataCleansing/cleansing.const.tsx @@ -8,13 +8,7 @@ import { formatDateTime, formatExecutionDuration, } from "@/utils/unit"; -import { - ClockCircleOutlined, - PlayCircleOutlined, - CheckCircleOutlined, - AlertOutlined, - PauseCircleOutlined, -} from "@ant-design/icons"; + import { BrushCleaning, Layout } from "lucide-react"; export function getTaskStatusMap(t: (key: string) => string) { @@ -22,32 +16,32 @@ export function getTaskStatusMap(t: (key: string) => string) { [TaskStatus.PENDING]: { label: t("dataCleansing.status.pending"), value: TaskStatus.PENDING, - color: "gray", - icon: , + color: "default", }, [TaskStatus.RUNNING]: { label: t("dataCleansing.status.running"), value: TaskStatus.RUNNING, - color: "blue", - icon: , + color: "processing", }, [TaskStatus.COMPLETED]: { label: t("dataCleansing.status.completed"), value: TaskStatus.COMPLETED, - color: "green", - icon: , + color: "success", + }, + [TaskStatus.PARTIAL_SUCCESS]: { + label: t("dataCleansing.status.partialSuccess"), + value: TaskStatus.PARTIAL_SUCCESS, + color: "warning", }, [TaskStatus.FAILED]: { label: t("dataCleansing.status.failed"), value: TaskStatus.FAILED, - color: "red", - icon: , + color: "error", }, [TaskStatus.STOPPED]: { label: t("dataCleansing.status.stopped"), value: TaskStatus.STOPPED, - color: "orange", - icon: , + color: "default", }, }; } diff --git a/frontend/src/pages/DataCleansing/cleansing.model.ts b/frontend/src/pages/DataCleansing/cleansing.model.ts index 5a109b6ca..7b5bd1176 100644 --- a/frontend/src/pages/DataCleansing/cleansing.model.ts +++ b/frontend/src/pages/DataCleansing/cleansing.model.ts @@ -56,6 +56,7 @@ export enum TaskStatus { PENDING = "PENDING", RUNNING = "RUNNING", COMPLETED = "COMPLETED", + PARTIAL_SUCCESS = "PARTIAL_SUCCESS", FAILED = "FAILED", STOPPED = "STOPPED", } diff --git a/frontend/src/pages/Layout/Sidebar.tsx b/frontend/src/pages/Layout/Sidebar.tsx index 8926451fd..b7c2d8fd9 100644 --- a/frontend/src/pages/Layout/Sidebar.tsx +++ b/frontend/src/pages/Layout/Sidebar.tsx @@ -168,7 +168,7 @@ const AsiderAndHeaderLayout = () => { height="100%" open={settingVisible} onClose={() => dispatch(hideSettings())} - bodyStyle={{ padding: 0 }} + styles={{ body: { padding: 0 } }} destroyOnHidden={true} > diff --git a/frontend/src/pages/OperatorMarket/Create/components/ConfigureStep.tsx b/frontend/src/pages/OperatorMarket/Create/components/ConfigureStep.tsx index e79df9ab2..aa957821b 100644 --- a/frontend/src/pages/OperatorMarket/Create/components/ConfigureStep.tsx +++ b/frontend/src/pages/OperatorMarket/Create/components/ConfigureStep.tsx @@ -1,10 +1,19 @@ -import { Alert, Input, Form } from "antd"; +import { Alert, Input, Form, Select } from "antd"; import TextArea from "antd/es/input/TextArea"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import ParamConfig from "@/pages/DataCleansing/Create/components/ParamConfig.tsx"; import { ChevronRight, Plus, Trash2 } from "lucide-react"; import { MetricI } from "@/pages/OperatorMarket/operator.model.ts"; +import type { MediaType } from "@/pages/OperatorMarket/operator.const.tsx"; + +const MEDIA_TYPE_OPTIONS: { label: string; value: MediaType }[] = [ + { label: "Text", value: "text" }, + { label: "Image", value: "image" }, + { label: "Audio", value: "audio" }, + { label: "Video", value: "video" }, + { label: "Multimodal", value: "multimodal" }, +]; export default function ConfigureStep({ parsedInfo, @@ -127,14 +136,22 @@ export default function ConfigureStep({ name="inputs" rules={[{ required: true }]} > - + +