feat: 编辑UI
This commit is contained in:
parent
9ab6726e17
commit
25fa8a043a
|
|
@ -384,6 +384,16 @@ def create_app():
|
|||
res.append(obj)
|
||||
return TableResponse(data=res, total=len(res))
|
||||
|
||||
@app.get("/api/flows/detail", tags=["Flow"], description="任务详情", response_model=BaseResponse)
|
||||
async def flows_detail(work_uuid: str, user: DB.User = Depends(get_current_user)):
|
||||
work = await DB.Work.filter(uuid=work_uuid).first()
|
||||
if not work:
|
||||
return BaseResponse(success=False, message="not found")
|
||||
work_content = await DB.WorkContent.filter(work_id=work.id).first()
|
||||
res = work.json()
|
||||
res["content"] = work_content.json()
|
||||
return BaseResponse(success=True, data=res)
|
||||
|
||||
@app.get("/api/flows/status", tags=["Flow"], description="获取任务状态")
|
||||
async def flows_status(token: str = ""):
|
||||
# 如果出现长时间链接被中断,可以设置 nginx 的 proxy_read_timeout
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ export const flowStop = async (id, data = {}) => {
|
|||
return request("/api/stop?work_uuid=" + id, "POST", data)
|
||||
}
|
||||
|
||||
export const flowDetail = async (id) => {
|
||||
return request("/api/flows/detail?work_uuid=" + id, "GET")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@
|
|||
padding: 16px 8px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Button, Card, Col, Row, Typography } from "antd"
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCallback } from 'react';
|
||||
import { Button, Card, Col, message, Row, Typography } from "antd"
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
MiniMap,
|
||||
|
|
@ -10,8 +10,12 @@ import {
|
|||
useEdgesState,
|
||||
addEdge,
|
||||
Position,
|
||||
useReactFlow,
|
||||
ReactFlowProvider,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { flowDetail } from "../../api/flow";
|
||||
import EditNode from "./components/EditNode";
|
||||
|
||||
const COLORS = {
|
||||
input: "#89A8B2",
|
||||
|
|
@ -32,23 +36,18 @@ const initialNodes = [
|
|||
backgroundColor: COLORS["input"],
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
position: { x: 200, y: 0 },
|
||||
data: { label: '2' },
|
||||
sourcePosition: 'right',
|
||||
targetPosition: 'left',
|
||||
},
|
||||
];
|
||||
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
|
||||
|
||||
|
||||
|
||||
const FlowDetail = () => {
|
||||
const { work_uuid } = useParams()
|
||||
const navigate = useNavigate();
|
||||
const backButton = <Button onClick={() => navigate("/flow")}>返回</Button>
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const [record, setRecord] = useState({});
|
||||
const editRef = useRef(null);
|
||||
const controlRef = useRef(null);
|
||||
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
const onBeforeDelete = async (params) => {
|
||||
if (params?.nodes?.find(e => e.type === "input")) {
|
||||
|
|
@ -56,40 +55,119 @@ const FlowDetail = () => {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
return <Row gutter={[16, 0]}>
|
||||
<Col span={18} >
|
||||
<Card title="工作" extra={backButton}>
|
||||
<div style={{ height: 'calc(100vh - 220px)', width: '100%' }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
onBeforeDelete={onBeforeDelete}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
fitView
|
||||
defaultViewport={{ zoom: 0.1, x: 0, y: 0}}
|
||||
style={{ background: '#f2f3f5' }}
|
||||
position={Position.Right}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<RightCard />
|
||||
</Col>
|
||||
</Row>
|
||||
const onAdd = (type) => {
|
||||
setNodes([
|
||||
...nodes,
|
||||
{
|
||||
id: new Date().getTime() + '_' + nodes.length,
|
||||
position: { x: nodes.length * 5, y: 0 },
|
||||
data: { label: '2' },
|
||||
sourcePosition: 'right',
|
||||
targetPosition: 'left',
|
||||
style: { backgroundColor: COLORS[type] },
|
||||
type: type,
|
||||
},
|
||||
]);
|
||||
}
|
||||
const onNodeClick = (_, record) => {
|
||||
if (record?.type === 'input') return;
|
||||
editRef?.current?.open(record);
|
||||
}
|
||||
const loadDetail = async () => {
|
||||
try {
|
||||
const res = await flowDetail(work_uuid);
|
||||
const { data: { content: { content: infoList } } } = res;
|
||||
let startX = 0;
|
||||
let startIndex = 1;
|
||||
if (res?.success) {
|
||||
setRecord(res.data);
|
||||
const tempEdges = [];
|
||||
const tempNodes = [{
|
||||
id: '1',
|
||||
position: { x: startX, y: 0 },
|
||||
sourcePosition: 'right',
|
||||
type: 'input',
|
||||
style: { backgroundColor: 'input' },
|
||||
data: { label: '开始' },
|
||||
}]
|
||||
infoList.forEach((e, i) => {
|
||||
tempNodes.push({
|
||||
id: e.uuid,
|
||||
position: { x: startX + 200, y: 0 },
|
||||
sourcePosition: 'right',
|
||||
targetPosition: 'left',
|
||||
type: e.type,
|
||||
style: { backgroundColor: COLORS[e.type] },
|
||||
data: { label: e.title },
|
||||
detail: e
|
||||
})
|
||||
|
||||
const sourceNode = tempNodes[startIndex - 1];
|
||||
const targetNode = tempNodes[startIndex];
|
||||
tempEdges.push({
|
||||
id: `${sourceNode.id}-${targetNode.id}`,
|
||||
source: sourceNode.id,
|
||||
target: targetNode.id
|
||||
})
|
||||
|
||||
startX += 200;
|
||||
startIndex += 1;
|
||||
|
||||
});
|
||||
|
||||
setNodes(tempNodes);
|
||||
setEdges(tempEdges);
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadDetail();
|
||||
}, [work_uuid])
|
||||
|
||||
return <ReactFlowProvider>
|
||||
<Row gutter={[16, 0]}>
|
||||
<Col span={18} >
|
||||
<Card title="工作" extra={backButton}>
|
||||
<div style={{ height: 'calc(100vh - 220px)', width: '100%' }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
onBeforeDelete={onBeforeDelete}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onNodeDoubleClick={onNodeClick}
|
||||
style={{ background: '#f2f3f5' }}
|
||||
position={Position.Right}
|
||||
fitView
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
<ControlButtons onLoad={obj => (controlRef.current = obj)} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<RightCard onCreate={onAdd} />
|
||||
<EditNode ref={editRef} />
|
||||
</Col>
|
||||
</Row>
|
||||
</ReactFlowProvider>
|
||||
}
|
||||
|
||||
const RightCard = ({ onCreate}) => {
|
||||
const nodes = [{ title: 'Git' }, {title: "Cmd"}, {title: "Status"}];
|
||||
const RightCard = ({ onCreate }) => {
|
||||
const nodes = [{ title: 'Git' }, { title: "Cmd" }, { title: "Status" }];
|
||||
return <Card title="节点" className="flow-detail-right">
|
||||
{nodes?.map((e, i) =>
|
||||
<div
|
||||
key={e.title}
|
||||
className="node"
|
||||
style={{ backgroundColor: COLORS[e.title.toLowerCase()] }}
|
||||
onClick={() => {
|
||||
|
|
@ -104,4 +182,12 @@ const RightCard = ({ onCreate}) => {
|
|||
</Card>
|
||||
}
|
||||
|
||||
const ControlButtons = ({ onLoad }) => {
|
||||
const reactFlow = useReactFlow();
|
||||
useEffect(() => {
|
||||
reactFlow && onLoad(reactFlow)
|
||||
}, [reactFlow])
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default FlowDetail;
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { DrawerForm, ModalForm, ProFormSelect, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
|
||||
import { Button, Flex, Space } from "antd";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
|
||||
const EditNode = forwardRef(({ onFinish }, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const formRef = useRef(null);
|
||||
const [record, setRecord] = useState(null);
|
||||
const [clients, setClients] = useState([{ label: "Local", value: 'local'}]);
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: (data) => {
|
||||
setOpen(true);
|
||||
setRecord(data);
|
||||
}
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
if (record && formRef.current) {
|
||||
formRef.current.setFieldsValue(record.detail);
|
||||
}
|
||||
}, [record, formRef.current]);
|
||||
|
||||
return <DrawerForm
|
||||
title="编辑"
|
||||
trigger={<></>}
|
||||
open={open}
|
||||
onOpenChange={e => setOpen(e)}
|
||||
formRef={formRef}
|
||||
width={600}
|
||||
drawerProps={{ maskClosable: false, destroyOnHidden: true}}
|
||||
>
|
||||
<ProFormText name='uuid' disabled label='UUID' />
|
||||
<Flex align="center">
|
||||
<ProFormSelect width={480} label="目标" rules={[{ required: true }]} name="client" options={clients} />
|
||||
<Button style={{ marginLeft: 6, marginTop: 6}}>添加</Button>
|
||||
</Flex>
|
||||
<ProFormText name='title' rules={[{ required: true}]} label='标题' fieldProps={{ maxLength: 20}} />
|
||||
<ProFormTextArea fieldProps={{ maxLength: 50}} name='description' label='描述' />
|
||||
|
||||
</DrawerForm>
|
||||
})
|
||||
|
||||
export default EditNode;
|
||||
Loading…
Reference in New Issue