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)
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)
async def flows_detail(work_uuid: str, user: DB.User = Depends(get_current_user)):
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)
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()
@app.post("/api/clients", tags=["Client"], description="终端创建", response_model=BaseResponse)
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()
@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)):
obj = await DB.Client.filter(id=client_id).first()
async def update_client(id:int, body: ClientRequest, user: DB.User = Depends(get_current_user)):
obj = await DB.Client.filter(id=id).first()
if obj:
obj.update_from_dict(body.model_dump())
obj.update_time = datetime.datetime.now()
@ -440,8 +444,8 @@ def create_app():
return 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)):
await DB.Client.filter(id=client_id).delete()
async def del_client(id: int, user: DB.User = Depends(get_current_user)):
await DB.Client.filter(id=id).delete()
return BaseResponse()
return app

View File

@ -1,11 +1,29 @@
import { ModalForm, ProForm } from "@ant-design/pro-components";
import { Button } from "antd";
import { ModalForm, ProFormDigit, ProFormText } from "@ant-design/pro-components";
import { Button, message } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import { create } from "../../../api/client";
const required = [{ required: true}]
const Create = ({ onFinish }) => {
return <ModalForm
title="创建节点"
modalProps={{ destroyOnHidden: true }}
initialValues={{ port: 22}}
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;

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

View File

@ -16,28 +16,40 @@ import {
import '@xyflow/react/dist/style.css';
import { flowDetail } from "../../api/flow";
import EditNode from "./components/EditNode";
import CustomNode from "./components/CustomNode";
const COLORS = {
input: "#89A8B2",
first: "#89A8B2",
git: "#B3C8CF",
cmd: "#E5E1DA",
status: "#F1F0E8"
}
const POSITION = {
first: ['right'],
git: ['right', 'left'],
cmd: ['right', 'left'],
status: ['left'],
}
const initialNodes = [
{
id: '1',
position: { x: 0, y: 0 },
data: { label: '开始' },
data: { label: '开始', style: { backgroundColor: COLORS['first'] }, position: POSITION['first'] },
sourcePosition: 'right',
targetPosition: 'left',
type: 'input',
style: {
backgroundColor: COLORS["input"],
}
type: 'first',
},
];
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
const initialEdges = [];
const nodeTypes = {
first: CustomNode,
git: CustomNode,
cmd: CustomNode,
status: CustomNode,
}
const FlowDetail = () => {
const { work_uuid } = useParams()
@ -55,22 +67,32 @@ const FlowDetail = () => {
}
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) => {
setNodes([
...nodes,
{
id: new Date().getTime() + '_' + nodes.length,
id: 'temp_' + new Date().getTime() + '_' + nodes.length,
position: { x: nodes.length * 5, y: 0 },
data: { label: '2' },
data: { label: type, style: { backgroundColor: COLORS[type] }, position: POSITION[type] },
sourcePosition: 'right',
targetPosition: 'left',
style: { backgroundColor: COLORS[type] },
type: type,
},
]);
}
const onNodeClick = (_, record) => {
if (record?.type === 'input') return;
if (record?.type === 'first') return;
editRef?.current?.open(record);
}
const loadDetail = async () => {
@ -86,9 +108,8 @@ const FlowDetail = () => {
id: '1',
position: { x: startX, y: 0 },
sourcePosition: 'right',
type: 'input',
style: { backgroundColor: 'input' },
data: { label: '开始' },
type: 'first',
data: { label: '开始', style: { backgroundColor: COLORS['first'] }, position: POSITION['first'] },
}]
infoList.forEach((e, i) => {
tempNodes.push({
@ -97,8 +118,7 @@ const FlowDetail = () => {
sourcePosition: 'right',
targetPosition: 'left',
type: e.type,
style: { backgroundColor: COLORS[e.type] },
data: { label: e.title },
data: { label: e.title, style: { backgroundColor: COLORS[e.type] }, position: POSITION[e.type] },
detail: e
})
@ -144,6 +164,8 @@ const FlowDetail = () => {
onNodeDoubleClick={onNodeClick}
style={{ background: '#f2f3f5' }}
position={Position.Right}
nodeTypes={nodeTypes}
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
fitView
>
<MiniMap />
@ -156,7 +178,7 @@ const FlowDetail = () => {
</Col>
<Col span={6}>
<RightCard onCreate={onAdd} />
<EditNode ref={editRef} />
<EditNode ref={editRef} onFinish={onEdit} />
</Col>
</Row>
</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 { Button, Flex, Space } from "antd";
import { DrawerForm, ModalForm, ProFormDigit, ProFormGroup, ProFormSelect, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
import { Button, Divider, Flex, Space, Typography } from "antd";
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
import { tableList } from "../../../api/client";
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'}]);
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, () => ({
open: (data) => {
setOpen(true);
setRecord(data);
setTimeout(() => setRecord(data), 100);
}
}))
useEffect(() => {
if (record && formRef.current) {
if (record && formRef.current && open) {
formRef.current.setFieldsValue(record.detail);
}
}, [record, formRef.current]);
}, [record, open, formRef.current]);
return <DrawerForm
return <ModalForm
title="编辑"
trigger={<></>}
open={open}
onOpenChange={e => setOpen(e)}
formRef={formRef}
width={600}
drawerProps={{ maskClosable: false, destroyOnHidden: true}}
onFinish={onSubmit}
modalProps={{ 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='描述' />
<Divider orientation="left">基础信息</Divider>
<ProFormGroup>
<ProFormText name='uuid' disabled label='UUID' width='md'/>
<ProFormSelect label="节点" rules={[{ required: true }]} name={["detail", "client"]} options={clients} request={onRequest} width='md' />
<ProFormText name='title' rules={[{ required: true }]} label='标题' fieldProps={{ maxLength: 20 }} width='md' />
<ProFormText fieldProps={{ maxLength: 50 }} name='description' label='描述' width="md" />
</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;

View File

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