feat: DAG画布嵌入项目详情页,任务节点点击弹详情Modal
This commit is contained in:
@@ -14,8 +14,6 @@ import {
|
||||
import type { Node, Edge, Connection, NodeProps } from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import {
|
||||
Card,
|
||||
Select,
|
||||
Button,
|
||||
Space,
|
||||
message,
|
||||
@@ -30,9 +28,10 @@ import {
|
||||
ReloadOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { projectApi, dagApi } from '@/api';
|
||||
import type { Project, TaskStatus, DagGraphNode, DagGraphEdge } from '@/types';
|
||||
import { dagApi } from '@/api';
|
||||
import type { TaskStatus, DagGraphNode, DagGraphEdge } from '@/types';
|
||||
import { taskStatusConfig, priorityConfig } from '@/components/constants';
|
||||
import TaskDetailModal from '@/components/TaskDetailModal';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -52,6 +51,7 @@ function TaskNode({ data }: NodeProps) {
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Handle type="target" position={Position.Top} />
|
||||
@@ -121,23 +121,19 @@ function layoutNodes(graphNodes: DagGraphNode[], graphEdges: DagGraphEdge[]) {
|
||||
return positions;
|
||||
}
|
||||
|
||||
export default function DagCanvas() {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [projectId, setProjectId] = useState<number | undefined>();
|
||||
interface Props {
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
/** DAG 编排画布(嵌入项目详情页使用) */
|
||||
export default function DagCanvas({ projectId }: Props) {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [executing, setExecuting] = useState(false);
|
||||
const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
|
||||
|
||||
// Load projects
|
||||
useEffect(() => {
|
||||
projectApi.list().then((ps) => {
|
||||
setProjects(ps);
|
||||
if (ps.length > 0 && !projectId) setProjectId(ps[0].id);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const [detailTaskId, setDetailTaskId] = useState<number | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
|
||||
const loadGraph = useCallback(async (pid: number) => {
|
||||
setLoading(true);
|
||||
@@ -182,7 +178,7 @@ export default function DagCanvas() {
|
||||
// Connect: add dependency (drag from source(bottom) to target(top))
|
||||
const onConnect = useCallback(
|
||||
async (conn: Connection) => {
|
||||
if (!conn.source || !conn.target || !projectId) return;
|
||||
if (!conn.source || !conn.target) return;
|
||||
try {
|
||||
await dagApi.addDependency({
|
||||
task_id: Number(conn.target),
|
||||
@@ -205,9 +201,15 @@ export default function DagCanvas() {
|
||||
message.error(e.response?.data?.detail || '添加依赖失败');
|
||||
}
|
||||
},
|
||||
[projectId, setEdges]
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
// Node click -> open task detail modal
|
||||
const onNodeClick = useCallback((_: unknown, node: Node) => {
|
||||
setDetailTaskId(Number(node.id));
|
||||
setDetailOpen(true);
|
||||
}, []);
|
||||
|
||||
// Delete selected edge
|
||||
const removeSelectedEdge = useCallback(async () => {
|
||||
if (!selectedEdge) return;
|
||||
@@ -269,70 +271,73 @@ export default function DagCanvas() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
title="DAG 编排画布"
|
||||
extra={
|
||||
<Space>
|
||||
<Select
|
||||
style={{ width: 220 }}
|
||||
placeholder="选择项目"
|
||||
value={projectId}
|
||||
onChange={setProjectId}
|
||||
options={projects.map((p) => ({ label: p.name, value: p.id }))}
|
||||
/>
|
||||
<Tooltip title="刷新">
|
||||
<Button icon={<ReloadOutlined />} onClick={() => projectId && loadGraph(projectId)} loading={loading} />
|
||||
</Tooltip>
|
||||
<Tooltip title={selectedEdge ? '删除选中的依赖连线' : '先点击选中一条连线'}>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={!selectedEdge}
|
||||
onClick={removeSelectedEdge}
|
||||
>
|
||||
删除依赖
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={executeAll}
|
||||
loading={executing}
|
||||
disabled={nodes.length === 0}
|
||||
>
|
||||
执行 DAG
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
操作说明:从节点底部拖拽到另一节点顶部可建立依赖(上游 → 下游);点击连线可选中并删除;支持拖拽移动节点、缩放画布。
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ height: 'calc(100vh - 260px)', minHeight: 480, border: '1px solid #f0f0f0', borderRadius: 8 }}>
|
||||
{nodes.length === 0 && !loading ? (
|
||||
<Empty description="该项目暂无任务,请先在任务管理中创建任务" style={{ marginTop: 120 }} />
|
||||
) : (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onEdgeClick={(_, edge) => setSelectedEdge(edge)}
|
||||
onPaneClick={() => setSelectedEdge(null)}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
deleteKeyCode={[]}
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
拖拽节点底部锚点到另一节点顶部建立依赖;点击节点查看详情;点击连线可选中删除。
|
||||
</Text>
|
||||
<Space>
|
||||
<Tooltip title="刷新">
|
||||
<Button size="small" icon={<ReloadOutlined />} onClick={() => loadGraph(projectId)} loading={loading} />
|
||||
</Tooltip>
|
||||
<Tooltip title={selectedEdge ? '删除选中的依赖连线' : '先点击选中一条连线'}>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={!selectedEdge}
|
||||
onClick={removeSelectedEdge}
|
||||
>
|
||||
<Background gap={16} />
|
||||
<Controls />
|
||||
{minimap}
|
||||
</ReactFlow>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
删除依赖
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={executeAll}
|
||||
loading={executing}
|
||||
disabled={nodes.length === 0}
|
||||
>
|
||||
执行 DAG
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ height: 560, border: '1px solid #f0f0f0', borderRadius: 8 }}>
|
||||
{nodes.length === 0 && !loading ? (
|
||||
<Empty description="该项目暂无任务,请先在任务管理中创建任务" style={{ marginTop: 160 }} />
|
||||
) : (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onNodeClick={onNodeClick}
|
||||
onEdgeClick={(_, edge) => setSelectedEdge(edge)}
|
||||
onPaneClick={() => setSelectedEdge(null)}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
deleteKeyCode={[]}
|
||||
>
|
||||
<Background gap={16} />
|
||||
<Controls />
|
||||
{minimap}
|
||||
</ReactFlow>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TaskDetailModal
|
||||
taskId={detailTaskId}
|
||||
open={detailOpen}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user