feat: 更新节点

This commit is contained in:
李如威 2025-09-17 10:11:59 +08:00
parent 03885d5a52
commit 78067519c3
9 changed files with 193 additions and 58 deletions

View File

@ -386,6 +386,10 @@ def create_app():
res.append(obj) res.append(obj)
return TableResponse(data=res, total=len(res)) return TableResponse(data=res, total=len(res))
@app.put("/api/flows", tags=["Flow"], description="任务修改", response_model=BaseResponse)
async def update_flow(work_uuid: int, body: ClientRequest, user: DB.User = Depends(get_current_user)):
return BaseResponse()
@app.get("/api/flows/detail", tags=["Flow"], description="任务详情", response_model=BaseResponse) @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)): async def flows_detail(work_uuid: str, user: DB.User = Depends(get_current_user)):
work = await DB.Work.filter(uuid=work_uuid).first() work = await DB.Work.filter(uuid=work_uuid).first()
@ -422,17 +426,17 @@ def create_app():
@app.post("/api/clients", tags=["Client"], description="终端创建", response_model=BaseResponse) @app.post("/api/clients", tags=["Client"], description="终端创建", response_model=BaseResponse)
async def create_client(body: ClientRequest, user: DB.User = Depends(get_current_user)): async def create_client(body: ClientRequest, user: DB.User = Depends(get_current_user)):
await DB.Client(**body, user_id=user.id).save() await DB.Client(**body.model_dump(), user_id=user.id).save()
return BaseResponse() return BaseResponse()
@app.post("/api/clients", tags=["Client"], description="终端创建", response_model=BaseResponse) @app.post("/api/clients", tags=["Client"], description="终端创建", response_model=BaseResponse)
async def create_client(body: ClientRequest, user: DB.User = Depends(get_current_user)): async def create_client(body: ClientRequest, user: DB.User = Depends(get_current_user)):
await DB.Client(**body, user_id=user.id).save() await DB.Client(**body.model_dump, user_id=user.id).save()
return BaseResponse() return BaseResponse()
@app.put("/api/clients", tags=["Client"], description="终端修改", response_model=BaseResponse) @app.put("/api/clients", tags=["Client"], description="终端修改", response_model=BaseResponse)
async def update_client(client_id:int, body: ClientRequest, user: DB.User = Depends(get_current_user)): async def update_client(id:int, body: ClientRequest, user: DB.User = Depends(get_current_user)):
obj = await DB.Client.filter(id=client_id).first() obj = await DB.Client.filter(id=id).first()
if obj: if obj:
obj.update_from_dict(body.model_dump()) obj.update_from_dict(body.model_dump())
obj.update_time = datetime.datetime.now() obj.update_time = datetime.datetime.now()
@ -440,8 +444,8 @@ def create_app():
return BaseResponse() return BaseResponse()
@app.delete("/api/clients", tags=["Client"], description="终端删除", response_model=BaseResponse) @app.delete("/api/clients", tags=["Client"], description="终端删除", response_model=BaseResponse)
async def del_client(client_id: int, user: DB.User = Depends(get_current_user)): async def del_client(id: int, user: DB.User = Depends(get_current_user)):
await DB.Client.filter(id=client_id).delete() await DB.Client.filter(id=id).delete()
return BaseResponse() return BaseResponse()
return app return app

View File

@ -1,11 +1,29 @@
import { ModalForm, ProForm } from "@ant-design/pro-components"; import { ModalForm, ProFormDigit, ProFormText } from "@ant-design/pro-components";
import { Button } from "antd"; import { Button, message } from "antd";
import { PlusOutlined } from "@ant-design/icons"; import { PlusOutlined } from "@ant-design/icons";
import { create } from "../../../api/client";
const required = [{ required: true}]
const Create = ({ onFinish }) => { const Create = ({ onFinish }) => {
return <ModalForm return <ModalForm
title="创建节点"
modalProps={{ destroyOnHidden: true }}
initialValues={{ port: 22}}
trigger={<Button icon={<PlusOutlined />} type='primary'>创建</Button>} trigger={<Button icon={<PlusOutlined />} type='primary'>创建</Button>}
></ModalForm> onFinish={async params => {
await create(params);
message.success("创建成功");
onFinish && onFinish();
return true;
}}
>
<ProFormText label="名字" name="name" rules={required} />
<ProFormText label="地址" name="host" rules={required} />
<ProFormDigit label="端口" name="port" rules={required} />
<ProFormText label="用户" name="user" rules={required} />
<ProFormText.Password label="密码" name="password" rules={required} />
</ModalForm>
} }
export default Create; export default Create;

View File

@ -0,0 +1,28 @@
import { ModalForm, ProFormDigit, ProFormText } from "@ant-design/pro-components";
import { Button, message } from "antd";
import { update } from "../../../api/client";
const required = [{ required: true}]
const Edit = ({ data, onFinish }) => {
return <ModalForm
title="编辑"
modalProps={{ destroyOnHidden: true }}
initialValues={{ ...data }}
trigger={<Button type="link" style={{ padding: '0 0', border: 'none' }}>编辑</Button>}
onFinish={async params => {
await update(data.id, params);
message.success("编辑成功");
onFinish && onFinish();
return true;
}}
>
<ProFormText label="名字" name="name" rules={required} />
<ProFormText label="地址" name="host" rules={required} />
<ProFormDigit label="端口" name="port" rules={required} />
<ProFormText label="用户" name="user" rules={required} />
<ProFormText.Password label="密码" name="password" rules={required} />
</ModalForm>
}
export default Edit;

View File

@ -1,9 +1,10 @@
import { ProTable } from "@ant-design/pro-components" import { ProTable } from "@ant-design/pro-components"
import { Card } from "antd" import { Button, Card, message, Popconfirm } from "antd"
import { useRef } from "react" import { useRef } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { tableList } from "../../api/client" import { del, tableList } from "../../api/client"
import Create from "./components/Create" import Create from "./components/Create"
import Edit from "./components/Edit"
const ClientPage = () => { const ClientPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -12,6 +13,11 @@ const ClientPage = () => {
const res = await tableList(); const res = await tableList();
return res; return res;
} }
const onDelete = async (record) => {
const res = await del(record.id);
message.success("删除成功");
actionRef?.current?.reload();
}
const columns = [ const columns = [
{ {
title: '#ID', title: '#ID',
@ -27,27 +33,28 @@ const ClientPage = () => {
}, },
{ {
title: '创建时间', title: '创建时间',
dataIndex: "update_time", dataIndex: "create_time",
render: (_, record) => new Date(record?.create_time * 1000).toLocaleString()
}, },
{ {
title: '操作', title: '操作',
valueType: 'option', valueType: 'option',
key: 'option', key: 'option',
with: 100, width: 100,
fixed: 'right', fixed: 'right',
render: (_, record) => [ render: (_, record) => [
<Button key='2' type="link" style={{ padding: '0 0', border: 'none' }} onClick={() => { <Edit data={record} key='1' onFinish={() => actionRef?.current?.reload()} />,
navigate("/flow/detail/" + record.uuid); <Popconfirm key='3' title="是否删除节点?" okText="删除" cancelText="取消" onConfirm={async () => await onDelete(record)}>
}}>编辑</Button>, <Button type="link" danger style={{ padding: '0 0', border: 'none' }}>删除</Button>
<Button key='3' type="link" danger style={{ padding: '0 0', border: 'none' }}>删除</Button>, </Popconfirm>
] ]
} }
] ]
return <Card title="节点管理"> return <Card title="节点管理" extra={<Create onFinish={() => actionRef?.current?.reload()} />}>
<ProTable <ProTable
toolBarRender={() => <Create />}
actionRef={actionRef} actionRef={actionRef}
rowKey='id' rowKey='id'
search={false} search={false}

View File

@ -16,28 +16,40 @@ import {
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import { flowDetail } from "../../api/flow"; import { flowDetail } from "../../api/flow";
import EditNode from "./components/EditNode"; import EditNode from "./components/EditNode";
import CustomNode from "./components/CustomNode";
const COLORS = { const COLORS = {
input: "#89A8B2", first: "#89A8B2",
git: "#B3C8CF", git: "#B3C8CF",
cmd: "#E5E1DA", cmd: "#E5E1DA",
status: "#F1F0E8" status: "#F1F0E8"
} }
const POSITION = {
first: ['right'],
git: ['right', 'left'],
cmd: ['right', 'left'],
status: ['left'],
}
const initialNodes = [ const initialNodes = [
{ {
id: '1', id: '1',
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
data: { label: '开始' }, data: { label: '开始', style: { backgroundColor: COLORS['first'] }, position: POSITION['first'] },
sourcePosition: 'right', sourcePosition: 'right',
targetPosition: 'left', targetPosition: 'left',
type: 'input', type: 'first',
style: {
backgroundColor: COLORS["input"],
}
}, },
]; ];
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }]; const initialEdges = [];
const nodeTypes = {
first: CustomNode,
git: CustomNode,
cmd: CustomNode,
status: CustomNode,
}
const FlowDetail = () => { const FlowDetail = () => {
const { work_uuid } = useParams() const { work_uuid } = useParams()
@ -55,22 +67,32 @@ const FlowDetail = () => {
} }
return true; return true;
} }
const onEdit = ({ id, ...detail }) => {
const temp = [...nodes];
temp.forEach(e => {
if (e.id == id) {
e.detail = detail;
e.data = { ...e.data, label: detail.title }
}
})
controlRef?.current?.updateNode(id);
setNodes(temp);
}
const onAdd = (type) => { const onAdd = (type) => {
setNodes([ setNodes([
...nodes, ...nodes,
{ {
id: new Date().getTime() + '_' + nodes.length, id: 'temp_' + new Date().getTime() + '_' + nodes.length,
position: { x: nodes.length * 5, y: 0 }, position: { x: nodes.length * 5, y: 0 },
data: { label: '2' }, data: { label: type, style: { backgroundColor: COLORS[type] }, position: POSITION[type] },
sourcePosition: 'right', sourcePosition: 'right',
targetPosition: 'left', targetPosition: 'left',
style: { backgroundColor: COLORS[type] },
type: type, type: type,
}, },
]); ]);
} }
const onNodeClick = (_, record) => { const onNodeClick = (_, record) => {
if (record?.type === 'input') return; if (record?.type === 'first') return;
editRef?.current?.open(record); editRef?.current?.open(record);
} }
const loadDetail = async () => { const loadDetail = async () => {
@ -86,9 +108,8 @@ const FlowDetail = () => {
id: '1', id: '1',
position: { x: startX, y: 0 }, position: { x: startX, y: 0 },
sourcePosition: 'right', sourcePosition: 'right',
type: 'input', type: 'first',
style: { backgroundColor: 'input' }, data: { label: '开始', style: { backgroundColor: COLORS['first'] }, position: POSITION['first'] },
data: { label: '开始' },
}] }]
infoList.forEach((e, i) => { infoList.forEach((e, i) => {
tempNodes.push({ tempNodes.push({
@ -97,8 +118,7 @@ const FlowDetail = () => {
sourcePosition: 'right', sourcePosition: 'right',
targetPosition: 'left', targetPosition: 'left',
type: e.type, type: e.type,
style: { backgroundColor: COLORS[e.type] }, data: { label: e.title, style: { backgroundColor: COLORS[e.type] }, position: POSITION[e.type] },
data: { label: e.title },
detail: e detail: e
}) })
@ -144,6 +164,8 @@ const FlowDetail = () => {
onNodeDoubleClick={onNodeClick} onNodeDoubleClick={onNodeClick}
style={{ background: '#f2f3f5' }} style={{ background: '#f2f3f5' }}
position={Position.Right} position={Position.Right}
nodeTypes={nodeTypes}
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
fitView fitView
> >
<MiniMap /> <MiniMap />
@ -156,7 +178,7 @@ const FlowDetail = () => {
</Col> </Col>
<Col span={6}> <Col span={6}>
<RightCard onCreate={onAdd} /> <RightCard onCreate={onAdd} />
<EditNode ref={editRef} /> <EditNode ref={editRef} onFinish={onEdit} />
</Col> </Col>
</Row> </Row>
</ReactFlowProvider> </ReactFlowProvider>

View File

@ -0,0 +1,15 @@
.custom-node {
height: 40.5px;
width: 150px;
border: 1px solid #eee;
padding: 5px;
border-radius: 5px;
background: white;
display: flex;
align-items: center;
justify-content: center;
}
.custom-node:hover {
border: 1px solid lightgray;
}

View File

@ -0,0 +1,17 @@
import React, { memo } from 'react';
import { Handle, Position } from '@xyflow/react';
import './CustomNode.css';
const CustomNode = memo(({ data: { style, position, ...data}, isConnectable }) => {
return (
<div className='custom-node' style={{ ...style }}>
<div >
{data.label}
</div>
{position?.includes('left') && <Handle type="target" position={Position.Left} isConnectable={isConnectable} />}
{position?.includes('right') && <Handle type="source" position={Position.Right} isConnectable={isConnectable} />}
</div>
);
})
export default CustomNode;

View File

@ -1,43 +1,71 @@
import { DrawerForm, ModalForm, ProFormSelect, ProFormText, ProFormTextArea } from "@ant-design/pro-components"; import { DrawerForm, ModalForm, ProFormDigit, ProFormGroup, ProFormSelect, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
import { Button, Flex, Space } from "antd"; import { Button, Divider, Flex, Space, Typography } from "antd";
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
import { tableList } from "../../../api/client";
const EditNode = forwardRef(({ onFinish }, ref) => { const EditNode = forwardRef(({ onFinish }, ref) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const formRef = useRef(null); const formRef = useRef(null);
const [record, setRecord] = useState(null); const [record, setRecord] = useState(null);
const [clients, setClients] = useState([{ label: "Local", value: 'local'}]); const [clients, setClients] = useState([{ label: "Local", value: 'local' }]);
const onRequest = async () => {
const res = await tableList();
return [{label: "[LOCAL]", value: 0},...res?.data?.map(e => ({ label: e.name, value: e.id }))];
}
const onSubmit = async (params) => {
onFinish && onFinish({ ...params, id: record?.id})
return true;
}
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: (data) => { open: (data) => {
setOpen(true); setOpen(true);
setRecord(data); setTimeout(() => setRecord(data), 100);
} }
})) }))
useEffect(() => { useEffect(() => {
if (record && formRef.current) { if (record && formRef.current && open) {
formRef.current.setFieldsValue(record.detail); formRef.current.setFieldsValue(record.detail);
} }
}, [record, formRef.current]); }, [record, open, formRef.current]);
return <DrawerForm return <ModalForm
title="编辑" title="编辑"
trigger={<></>} trigger={<></>}
open={open} open={open}
onOpenChange={e => setOpen(e)} onOpenChange={e => setOpen(e)}
formRef={formRef} formRef={formRef}
width={600} onFinish={onSubmit}
drawerProps={{ maskClosable: false, destroyOnHidden: true}} modalProps={{ maskClosable: false, destroyOnHidden: true }}
> >
<ProFormText name='uuid' disabled label='UUID' /> <Divider orientation="left">基础信息</Divider>
<Flex align="center"> <ProFormGroup>
<ProFormSelect width={480} label="目标" rules={[{ required: true }]} name="client" options={clients} /> <ProFormText name='uuid' disabled label='UUID' width='md'/>
<Button style={{ marginLeft: 6, marginTop: 6}}>添加</Button> <ProFormSelect label="节点" rules={[{ required: true }]} name={["detail", "client"]} options={clients} request={onRequest} width='md' />
</Flex> <ProFormText name='title' rules={[{ required: true }]} label='标题' fieldProps={{ maxLength: 20 }} width='md' />
<ProFormText name='title' rules={[{ required: true}]} label='标题' fieldProps={{ maxLength: 20}} /> <ProFormText fieldProps={{ maxLength: 50 }} name='description' label='描述' width="md" />
<ProFormTextArea fieldProps={{ maxLength: 50}} name='description' label='描述' /> </ProFormGroup>
{record?.type === 'git' && <>
<Divider orientation="left">Git</Divider>
<ProFormGroup>
<ProFormText name={["detail", "git_url"]} rules={[{ required: true }]} label='地址' width="md" />
<ProFormText name={["detail", "git_branch"]} rules={[{ required: true }]} label='分支' width='md' />
<ProFormSelect name={["detail", "git_clone_type"]} rules={[{ required: true }]} label='协议' width='md' options={["http/https"]} />
<ProFormDigit name={["detail", "git_clone_depth"]} rules={[{ required: true }]} label='Depth' width='md' />
<ProFormText name={["detail", "git_path"]} rules={[{ required: true }]} label='路径' width="md" />
<ProFormText name={["detail", "git_user"]} rules={[{ required: true }]} label='帐号' width='md' />
<ProFormText.Password name={["detail", "git_password"]} rules={[{ required: true }]} label='密码' width='md' />
</ProFormGroup>
</>}
{record?.type === 'cmd' && <>
<Divider orientation="left">命令</Divider>
<ProFormTextArea fieldProps={{ rows: 10 }} name={["detail", "cmd"]} rules={[{ required: true }]} label='命令' width={690} />
</>}
{record?.type === 'status' && <>
<Divider orientation="left">健康监测</Divider>
<ProFormText name={["detail", "status_url"]} rules={[{ required: true }]} label='检测地址' width={690} />
</>}
</DrawerForm> </ModalForm>
}) })
export default EditNode; export default EditNode;

View File

@ -129,7 +129,6 @@ export default () => {
} }
// //
useEffect(() => { useEffect(() => {
if (!pageInit.current) return;
if (!user?.token) return; if (!user?.token) return;
if (eventSourceRef.current) { if (eventSourceRef.current) {
eventSourceRef.current.close(); eventSourceRef.current.close();
@ -167,9 +166,6 @@ export default () => {
}; };
}, [user]); }, [user]);
useEffect(() => {
pageInit.current = true;
}, [])
return ( return (
<Card title="工作台"> <Card title="工作台">
<ProTable <ProTable