Commit 93b21ec8 authored by chenshouchao's avatar chenshouchao

feat: 新增批算子联调前提交

parent 86b7b820
......@@ -41,6 +41,8 @@ const RESTAPI = {
API_SAVE_USERSPEC:`${BACKEND_API_URI_PREFIX}/cpp/workflow/saveuserspec`, // 保存用户自定义工作流模板
API_OVERVIEW_GET:`${BACKEND_API_URI_PREFIX}/cpp/basicInformation`, // 获取概览基本信息
API_TASK_OVERVIEW_LIST:`${BACKEND_API_URI_PREFIX}/cpp/workflowJobInformation`, // 查询任务概览
API_OPERATOR_LISTSTREAMACTORS:`${BACKEND_API_URI_PREFIX}/cpp/workflow/liststreamactors`, // 获取流算子列表,可用于模糊查询,返回所有版本流算子
API_SAVE_BATCHACTOR:`${BACKEND_API_URI_PREFIX}/cpp/workflow/savebatchactor`, // 保存批算子
};
export default RESTAPI;
......@@ -270,6 +270,44 @@ const getTaskOverview=(params:getTaskOverviewParams)=>{
})
}
// 获取流算子列表,可用于模糊查询,返回所有版本流算子
type getOperatorListParams = {
productId: string;
keyword?: string;
page?: number;
size?: number;
};
const getOperatorList=(params:getOperatorListParams)=>{
return request({
url:Api.API_OPERATOR_LISTSTREAMACTORS,
method:"get",
params,
})
}
type saveBatchQuery = {
productId: string;
batchName: string;
batchVersion: string;
description?: string;
isEdit?: string;
}
type saveBatchBody = Array<any>
type saveBatchParams = {
query: saveBatchQuery;
body: saveBatchBody;
}
// 提交工作流
const saveBatchActor = (params: saveBatchParams) => {
return request({
url: Api.API_SAVE_BATCHACTOR,
method: "post",
params: params.query,
data: params.body,
});
};
export {
current,
......@@ -292,5 +330,7 @@ export {
submitWorkFlow,
getworkFlowTaskInfo,
getOverviewInfo,
getTaskOverview
getTaskOverview,
getOperatorList,
saveBatchActor
};
......@@ -4,7 +4,7 @@
align-items: center;
border: 1px solid #e6e8eb;
border-radius: 4px;
background-color: #F0F2F5;
background-color: #f0f2f5;
cursor: pointer;
height: 32px;
box-sizing: border-box;
......@@ -12,7 +12,7 @@
}
.radio {
position: relative;
min-width: 64px;
min-width: 63px;
height: 28px;
box-sizing: border-box;
font-size: 14px;
......@@ -28,18 +28,8 @@
white-space: nowrap;
}
.radio:not(:last-child)::before {
position: absolute;
width: 1px;
height: 16px;
top: 6px;
right: 0;
content: '';
background-color: #D1D6DE;
}
.radioActive {
color: #1370ff;
background-color: #fff;
box-shadow: 2px 4px 12px 0px rgba(0,27,63,0.06);
box-shadow: 2px 4px 12px 0px rgba(0, 27, 63, 0.06);
}
......@@ -38,22 +38,36 @@
}
.li {
background-color: RGBA(240, 242, 245, 1);
padding: 7px 7px 7px 28px;
padding: 7px 9px;
color: rgba(30, 38, 51, 1);
font-size: 14px;
line-height: 22px;
margin-bottom: 12px;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
}
.nameVersion {
flex: 1;
/* text-align: left; */
margin-left: 13px;
word-wrap: break-word;
/* text-overflow: clip; */
max-width: 140px;
}
.name {
margin-right: 8px;
}
.version {
white-space: nowrap;
}
.icon {
width: 6px;
height: 10px;
position: absolute;
/* position: absolute;
top: 13px;
left: 9px;
left: 9px; */
display: flex;
flex-direction: column;
justify-content: space-between;
......
import { InputBase, IconButton } from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import styles from "./index.module.css";
import cloneDeep from "lodash/cloneDeep";
import { getScrollLoader } from "@/utils/util";
import { useState } from "react";
import { useState, useCallback, useEffect } from "react";
import { getOperatorList } from "@/api/project_api";
import useMyRequest from "@/hooks/useMyRequest";
import { useStores } from "@/store";
import { toJS } from "mobx";
import { ITask } from "@/views/Project/ProjectSubmitWork/interface";
const OperatorList = () => {
const [name, setName] = useState("");
const nameChange = (e: any) => {
setName(e.target.value);
};
const list = [
{
name: "这是中文",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
{
name: "asdf",
version: "1.0.0",
},
];
let count = 1;
type IProps = {
operatorList: any;
setOperatorList: any;
};
let isInRequest = false;
const OperatorList = (props: IProps) => {
const { operatorList, setOperatorList } = props; // 流程图中流算子列表
const { currentProjectStore } = useStores();
const [list, setList] = useState<ITask[]>([]); // 算子列表
const productId = toJS(currentProjectStore.currentProductInfo.id);
const [keyword, setKeyword] = useState(""); // 搜索算子列表时的关键词
const [dragItem, setDragItem] = useState<any>({}); // 拖拽的算子
const [page, setPage] = useState(0); //
const [isLastPage, setIsLastPage] = useState(false);
const { run: getOperatorListRun } = useMyRequest(getOperatorList, {
onSuccess: (res) => {
isInRequest = false;
window.onscroll = (e) => {
console.log(e);
console.log("res", res);
let arr = res?.data?.content;
setIsLastPage(res?.data?.last);
arr.forEach((item: any, index: number) => {
item.edges = [];
item.parameters.push({
choices: [],
classType: "STRING",
classTypeName: "String",
defaultValue: null,
description: "",
domType: "INPUT",
hidden: false,
isnull: false,
level: null,
linked: false,
many: false,
name: `in${index + 1}`,
order: 0,
parameterGroup: "in",
promoted: false,
promotedName: null,
required: false,
title: null,
validators: [],
});
});
arr.forEach((item: any, index: number) => {
item.parameters.push({
choices: [],
classType: "STRING",
classTypeName: "String",
defaultValue: null,
description: "",
domType: "INPUT",
hidden: false,
isnull: false,
level: null,
linked: false,
many: false,
name: `out${index + 1}`,
order: 0,
parameterGroup: "out",
promoted: false,
promotedName: null,
required: false,
title: null,
validators: [],
});
});
setList(arr);
// setList(res?.data?.content || []);
},
onError: () => {
isInRequest = false;
},
});
const getOperatorListFun = useCallback(
(keyword: string = "", page = 0) => {
if (isInRequest) {
return;
} else {
getOperatorListRun({
productId: productId as string,
keyword,
page,
size: 5,
});
}
},
[productId, getOperatorListRun]
);
useEffect(() => {
getOperatorListFun();
}, [getOperatorListFun]);
const handleKeywordKeyUp = (e: any) => {
if (e.keyCode === 13) {
setKeyword(e.target.value);
setPage(0);
getOperatorListFun(e.target.value, 0);
}
};
// const list = [
// {
// title: "这是中文",
// version: "1.0.0",
// },
// {
// allVersions: ["1.0.0", "2.0.0"],
// creator: "root",
// description: "这是一段Hitlist算子的描述",
// edges: [
// {
// id: "fdd26229-3a1f-46e7-a14a-dd55bcce55e4",
// label: null,
// source: "62c7965c9bf7ba39f2dcefcc",
// sourceHandle: null,
// target: "62c7965c9bf7ba39f2dcefce",
// targetHandle: null,
// },
// ],
// id: "62c7965c9bf7ba39f2dcefcc",
// parameters: [
// {
// name: "cpus",
// classType: "INT",
// classTypeName: "Int",
// required: false,
// defaultValue: "1",
// },
// ],
// // parentNode: "62c7965c9bf7ba39f2dcefc8",
// position: { x: 40, y: 200 },
// productId: "cadd",
// status: null,
// title: "Hitlist",
// type: "FLOW",
// version: "2.0.0",
// },
// {
// title: "asdf",
// version: "1.0.0",
// },
// {
// title: "asdf",
// version: "1.0.0",
// },
// {
// title: "asdf",
// version: "1.0.0",
// },
// ];
const handleScroll = (e: any) => {
console.log(e);
if (getScrollLoader(e)) {
console.log("加载");
console.log("加载1");
if (isInRequest || isLastPage) {
return;
} else {
console.log("加载2");
getOperatorListFun(keyword, page + 1);
setPage(page + 1);
}
}
};
......@@ -362,6 +199,107 @@ const OperatorList = () => {
);
};
/** 拖拽开始 */
const onDragStart = (item: any) => {
setDragItem(item);
console.log("onDragStart");
// setIsDragStyle(true);
count++;
};
/** 生成批流副本 */
const getNewOperatorItem = useCallback(
(task: ITask, x: number, y: number) => {
const newEdges =
task?.edges &&
task?.edges.map((every) => {
return {
...every,
source: `${every.source}_${count}`,
target: `${every.target}_${count}`,
};
});
return {
...task,
id: `${task.id}_${count}`,
position: {
x: x,
y: y,
},
edges: newEdges.length ? newEdges : task?.edges,
};
// const newVal = arr.map((item) => {
// const newEdges =
// item?.edges &&
// item?.edges.map((every) => {
// return {
// ...every,
// source: `${every.source}_${count}`,
// target: `${every.target}_${count}`,
// };
// });
// return {
// ...item,
// id: `${item.id}_${count}`,
// parentNode: item.parentNode ? `${item.parentNode}_${count}` : "",
// position: {
// x: item.position?.x,
// y: item.position?.y,
// },
// edges: newEdges.length ? newEdges : item?.edges,
// };
// });
// return newVal;
},
[]
);
/** 拖拽结束 */
const onDragEnd = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
const dom = document.getElementById("customOperatorFlow");
console.log(e);
console.log(dom);
const clientX = e.clientX;
const clientY = e.clientY;
const upperLeftPointX = Number(dom?.offsetLeft);
const upperLeftPointY = Number(dom?.offsetTop);
const lowerRightX = Number(upperLeftPointX) + Number(dom?.offsetWidth);
const lowerRightY = Number(upperLeftPointY) + Number(dom?.offsetHeight);
if (
clientX > upperLeftPointX &&
clientY > upperLeftPointY &&
clientX < lowerRightX &&
clientY < lowerRightY
) {
// const batchFlowArr = getBatchFlowArr(info.id);
const newDragItem = cloneDeep(dragItem);
const newOperatorItem = getNewOperatorItem(
newDragItem,
clientX - upperLeftPointX,
clientY - upperLeftPointY - 42
);
// const newOperatorItem = {
// ...newDragItem,
// id: `${newDragItem.id}_${count}`,
// source: `${newDragItem.source}_${count}`,
// target: `${newDragItem.target}_${count}`,
// position: {
// x: clientX - upperLeftPointX,
// y: clientY - upperLeftPointY,
// },
// };
const newVal = cloneDeep(operatorList);
newVal.push(newOperatorItem);
console.log(newVal);
setOperatorList(newVal);
// setTemplateConfigInfo(newVal);
}
// setIsDragStyle(false);
},
[dragItem, operatorList, setOperatorList, getNewOperatorItem]
);
return (
<div className={styles.operatorList}>
<div className={styles.searchBox}>
......@@ -378,17 +316,28 @@ const OperatorList = () => {
<InputBase
className={styles.searchInput}
placeholder="请输入算子名称"
value={name}
onChange={nameChange}
// value={keyword}
// onChange={keywordChange}
onKeyUp={handleKeywordKeyUp}
/>
</div>
<div className={styles.list} onScroll={(e) => handleScroll(e)}>
{list.map((item, index) => {
return (
<div key={index} className={styles.li}>
<div
key={index}
className={styles.li}
draggable={true}
onDragStart={() => onDragStart(item)}
onDragEnd={onDragEnd}
>
{randerIcon()}
<span className={styles.name}>{item.name}</span>
<span className={styles.version}>{item.version}</span>
<div className={styles.nameVersion}>
<span className={styles.name}>{item.title}</span>
<span className={styles.version}>v{item.version}</span>
</div>
{/* <span className={styles.name}>{item.title}</span>
<span className={styles.version}>{item.version}</span> */}
</div>
);
})}
......
import { saveUserSpec } from "@/api/workbench_api";
import MyDialog from "@/components/mui/MyDialog";
import MyInput from "@/components/mui/MyInput";
import { checkIsNumberLetterChinese } from "@/utils/util";
import { useState } from "react";
import useMyRequest from "@/hooks/useMyRequest";
import { useStores } from "@/store";
import styles from "./index.module.css";
import { useMessage } from "@/components/MySnackbar";
import { toJS } from "mobx";
import { ITask } from "@/views/Project/ProjectSubmitWork/interface";
interface IProps {
saveFormDialog: boolean;
setSaveFormDialog: (val: boolean) => void;
onBack?: () => void;
title: string;
setTitle: (val: string) => void;
version: string;
setVersion: (val: string) => void;
description: string;
setDescription: (val: string) => void;
creator?: string;
templateConfigInfo: ITask[];
id?: string;
}
const SaveOperator = (props: IProps) => {
const {
saveFormDialog,
setSaveFormDialog,
onBack,
title,
setTitle,
version,
setVersion,
description,
setDescription,
templateConfigInfo,
creator,
id,
} = props;
const { currentProjectStore } = useStores();
const Message = useMessage();
const productId = toJS(currentProjectStore.currentProductInfo.id);
const [titleHelper, setTitleHelper] = useState({
// 算子名称错误提示
error: false,
helperText: "",
});
const [versionHelper, setVersionHelper] = useState({
// 算子版本错误提示
error: false,
helperText: "",
});
// 算子保存方法
const { run: saveUserSpecRun } = useMyRequest(saveUserSpec, {
onSuccess: (res) => {
Message.success("保存成功!");
onBack && onBack();
},
});
// 关闭表单弹窗
const handleCloseDialog = () => {
setSaveFormDialog(false);
};
// 算子名称
const handleTitleChange = (e: any) => {
const title = e.target.value;
setTitle(title);
checkTitle(title);
};
// 算子版本
const handleVersionChange = (e: any) => {
let version = e.target.value;
setVersion(version);
checkVersion(version);
};
// 算子描述
const handleDescriptionChange = (e: any) => {
let description = e.target.value;
if (description.length < 301) {
setDescription(description);
}
};
// 校验算子名称
const checkTitle = (title: string) => {
if (!title) {
setTitleHelper({
error: true,
helperText: "必须输入算子名称",
});
return false;
} else if (title.length > 15) {
setTitleHelper({
error: true,
helperText: "格式不正确,必须在15字符以内,仅限大小写字母、数字、中文",
});
return false;
} else if (!checkIsNumberLetterChinese(title)) {
setTitleHelper({
error: true,
helperText: "格式不正确,必须在15字符以内,仅限大小写字母、数字、中文",
});
return false;
} else {
setTitleHelper({
error: false,
helperText: "",
});
return true;
}
};
// 校验版本号格式
const checkVersion = (version: string) => {
if (/^[1-9]\d?(\.(0|[1-9]\d?)){2}$/.test(version)) {
setVersionHelper({
error: false,
helperText: "",
});
} else {
setVersionHelper({
error: true,
helperText: "格式不正确,必须为X.Y.Z格式,且XYZ必须为0~99的正整数",
});
return false;
}
};
// 表单弹窗确定,新建算子
const handleOncofirm = () => {
if (checkTitle(title) && checkVersion(version)) {
if (id) {
saveUserSpecRun({
title,
version,
description,
tasks: templateConfigInfo,
productId,
id,
creator,
});
} else {
saveUserSpecRun({
title,
version,
description,
tasks: templateConfigInfo,
productId,
});
}
}
};
return (
<MyDialog
open={saveFormDialog}
title="保存算子"
onClose={handleCloseDialog}
onConfirm={handleOncofirm}
>
<div className={styles.saveBox}>
<MyInput
value={title}
label="算子名称"
onChange={handleTitleChange}
required
error={titleHelper.error}
helperText={titleHelper.helperText}
style={{ margin: "20px 0" }}
disabled={id ? true : false}
></MyInput>
<MyInput
value={version}
label="版本号"
onChange={handleVersionChange}
error={versionHelper.error}
helperText={versionHelper.helperText}
style={{ marginBottom: "20px" }}
></MyInput>
<div style={{ position: "relative" }}>
<MyInput
value={description}
id="desc"
label="模板描述"
placeholder="模板描述"
onChange={handleDescriptionChange}
multiline
rows={4}
/>
<span
style={{
position: "absolute",
bottom: "7px",
right: "12px",
color: description.length >= 300 ? "#d32f2f" : "#C2C6CC",
}}
>
{description.length}/300
</span>
</div>
</div>
</MyDialog>
);
};
export default SaveOperator;
......@@ -3,7 +3,15 @@ import { observer } from "mobx-react-lite";
import FullScreenDrawer from "@/components/CommonComponents/FullScreenDrawer";
import MyButton from "@/components/mui/MyButton";
import OperatorList from "./components/OperatorList";
import Flow from "../Project/components/Flow";
// import Flow from "../Project/components/Flow";
import useMyRequest from "@/hooks/useMyRequest";
import { saveBatchActor } from "@/api/project_api";
import { useMessage } from "@/components/MySnackbar";
import { useStores } from "@/store";
import BatchOperatorFlow from "../Project/components/Flow/components/BatchOperatorFlow";
import { toJS } from "mobx";
import { ITask } from "../Project/ProjectSubmitWork/interface";
import _ from "lodash";
import styles from "./index.module.css";
type IProps = {
......@@ -12,24 +20,162 @@ type IProps = {
const CustomOperator = observer((props: IProps) => {
const { setShowCustomOperator } = props;
const Message = useMessage();
const [operatorList, setOperatorList] = useState<ITask[]>([]);
const { currentProjectStore } = useStores();
const productId = toJS(currentProjectStore.currentProductInfo.id);
// const [showCustomOperator, setShowCustomOperator] = useState(false);
/** 设置选中唯一标识符 */
const handleNodeClick = useCallback((val: string) => {
// setSelectTaskId(val);
// console.log(val);
}, []);
// 保存批算子
const { run: saveBatchActorRun } = useMyRequest(saveBatchActor, {
onSuccess: (res) => {
console.log("res", res);
},
});
const handleSave = useCallback(() => {
saveBatchActorRun({
query: {
productId: productId as string,
batchName: "123456",
batchVersion: "1.0.0",
description: "",
},
body: [],
});
}, [saveBatchActorRun, productId]);
// 判断 每个流算子必须至少有一条连接线。
const checkHasOneLine = (sourceArr: string[], targetArr: string[]) => {
const all = _.uniq([...sourceArr, ...targetArr]);
if (all.length === operatorList.length) {
return true;
} else {
return false;
}
// _.uniq([2, 1, 2]);
};
// 判断 每个起始算子(可以有多个起始点)的输入必须为文件的路径输入或数据集的路径输入。
const checkIn = (targetArr: string[]) => {
const uniqTargetArr = _.uniq(targetArr);
if (uniqTargetArr.length === operatorList.length) {
// 流节点连成一个圈了
return false;
}
let check = true;
operatorList.forEach((flowNode) => {
if (uniqTargetArr.indexOf(flowNode.id) === -1) {
// 该节点的输入没有连线 也就是说这个节点是起点
const inArr = flowNode.parameters.filter(
(parameter) => parameter.parameterGroup === "in"
);
if (inArr.length > 0) {
if (
!inArr.some((inItem) => {
return inItem.domType === "dataset" || inItem.domType === "path";
})
) {
check = false;
}
} else {
// 起点没有输入
check = false;
}
}
});
return check;
};
// 判断 起码有一个结尾算子(可以有多个结尾点)的输出必须为文件保存或数据集保存。
const checkOut = (sourceArr: string[]) => {
const uniqSourceArr = _.uniq(sourceArr);
if (uniqSourceArr.length === operatorList.length) {
// 流节点连成一个圈了
return false;
}
let check = true;
operatorList.forEach((flowNode) => {
if (uniqSourceArr.indexOf(flowNode.id) === -1) {
// 该节点的输入没有连线 也就是说这个节点是起点
const inArr = flowNode.parameters.filter(
(parameter) => parameter.parameterGroup === "in"
);
if (inArr.length > 0) {
if (
!inArr.some((inItem) => {
return inItem.domType === "dataset" || inItem.domType === "file";
})
) {
check = false;
}
} else {
// 起点没有输入
check = false;
}
}
});
return check;
};
const handleCheck = () => {
if (operatorList.length === 0) {
Message.error("内容不能为空!");
return;
}
let sourceArr: string[] = [];
let targetArr: string[] = [];
operatorList.forEach((flowNode) => {
flowNode.edges.forEach((edge) => {
edge.source && sourceArr.push(edge.source);
edge.target && targetArr.push(edge.target);
});
});
if (!checkHasOneLine([...sourceArr], [...targetArr])) {
Message.error("内容校验未通过,请检查!");
return;
}
if (!checkIn([...targetArr])) {
Message.error("内容校验未通过,请检查!");
return;
}
if (!checkOut([...sourceArr])) {
Message.error("内容校验未通过,请检查!");
return;
}
};
return (
<FullScreenDrawer handleClose={setShowCustomOperator} zIndex={1100}>
<div className={styles.customOperator}>
<div className={styles.coTop}>
<div className={styles.coTitle}>添加算子</div>
<MyButton text="添加"></MyButton>
<MyButton
text="添加"
onClick={() => {
handleCheck();
}}
></MyButton>
</div>
<div className={styles.coContent}>
<OperatorList />
<Flow
showControls={false}
// tasks={templateConfigInfo}
// setTasks={setTemplateConfigInfo}
<div className={styles.coContent} id="customOperatorFlow">
<OperatorList
operatorList={operatorList}
setOperatorList={setOperatorList}
/>
<BatchOperatorFlow
tasks={operatorList}
setTasks={setOperatorList}
type="edit"
// onFlowNodeClick={handleNodeClick}
onFlowNodeClick={handleNodeClick}
flowNodeDraggable={true}
// ListenState={!saveFormDialog}
showControls={false}
/>
</div>
</div>
......
// 自定义批算子时使用的流程图
import ReactFlow, {
Controls,
Background,
useNodesState,
useEdgesState,
ReactFlowProps,
Node,
Connection,
Edge,
} from "react-flow-renderer";
import { useCallback, useEffect, useMemo, useState } from "react";
import { uuid } from "@/utils/util";
import {
IEdge,
IParameter,
ITask,
} from "../../../../ProjectSubmitWork/interface";
import { ILine } from "../../interface";
import BatchNode from "../BatchNode";
import FlowNode from "../FlowNode";
import { getCustomTemplateParameterCheckResult } from "@/views/WorkFlowEdit/util";
import { useMessage } from "@/components/MySnackbar";
import styles from "./index.module.css";
interface IProps extends ReactFlowProps {
tasks?: ITask[];
/** 类型, edit为编辑类型 */
type?: "edit" | "default";
/** 设置组件数据 组件为编辑状态使用 */
setTasks?: (val: ITask[]) => void;
/** 点击流程node 节点 返回唯一标识符 */
onFlowNodeClick?: (val: string) => void;
/** 监听事件的状态 */
ListenState?: boolean;
/** 流节点是否可以拖拽 */
flowNodeDraggable?: boolean;
// 是否显示Controls(放大缩小全屏等按钮)
showControls?: boolean;
}
const BatchOperatorFlow = (props: IProps) => {
const {
tasks,
type: flowType = "default",
setTasks,
onFlowNodeClick,
ListenState = true,
flowNodeDraggable = false,
showControls = true,
...other
} = props;
/** 自定义的节点类型 */
const nodeTypes = useMemo(() => {
return { batchNode: BatchNode, flowNode: FlowNode };
}, []);
/** 内部维护的选择的flow节点Id */
const [inSideFlowNodeId, setInSideFlowNodeId] = useState<string>("");
/** 选中的线 */
const [selectedEdge, setSelectedEdge] = useState<Edge>();
const Message = useMessage();
/** 原始数据删除线 */
const tasksDeleteLine = useCallback(
(connection: Connection | Edge) => {
const result =
(tasks?.length &&
tasks.map((item) => {
/** 删除batch起始的edges中的一项 === 等于删除了一根连线 */
if (item.id === connection.source && item.type === "BATCH") {
const newEdges =
(item.edges?.length &&
item.edges?.filter(
(every) => every.sourceHandle !== connection.sourceHandle
)) ||
[];
return {
...item,
edges: newEdges,
};
/** 选中batch结束位置&&更新校验值 */
} else if (item.id === connection.target && item.type === "BATCH") {
const newParameters =
(item.parameters?.length &&
item.parameters.map((every) => {
if (every.name === connection.targetHandle) {
const { error, helperText } =
getCustomTemplateParameterCheckResult({
...every,
linked: false,
hidden: false,
});
return {
...every,
hidden: false,
error,
helperText,
};
} else {
return every;
}
})) ||
[];
return {
...item,
parameters: newParameters,
};
} else {
return item;
}
})) ||
[];
return result;
},
[tasks]
);
/** 删除流节点或者线 */
const deleteSelectFlowNode = useCallback(
(e: any) => {
if (e.keyCode === 8 && ListenState) {
/** 删除流节点逻辑 */
// inSideFlowNodeId
console.log(inSideFlowNodeId);
if (inSideFlowNodeId) {
const newVal =
(tasks?.length &&
tasks.filter((item) => {
return item.id !== inSideFlowNodeId;
})) ||
[];
setTasks && setTasks(newVal);
}
if (selectedEdge) {
const newVal = tasksDeleteLine(selectedEdge);
setTasks && setTasks(newVal);
}
}
},
[
inSideFlowNodeId,
ListenState,
tasks,
selectedEdge,
setTasks,
tasksDeleteLine,
]
);
/** 监听鼠标按下事件 */
useEffect(() => {
window.addEventListener("keyup", deleteSelectFlowNode);
return () => {
window.removeEventListener("keyup", deleteSelectFlowNode);
};
}, []);
/** 生成初始化node节点 */
const initialNodes = useMemo(() => {
const val: any = [];
tasks?.length &&
tasks.forEach((item) => {
val.push({
id: item.id,
type: item.type === "BATCH" ? "batchNode" : "flowNode",
/** 每一项的数据 */
data: {
info: item,
...{
selectedStatus: inSideFlowNodeId === item.id,
flowNodeStyle: {
backgroundColor: "#fff",
borderRadius: "4px",
},
inStyle: {
backgroundColor: "rgba(19, 112, 255, 1)",
border: "none",
left: 12,
},
outStyle: {
backgroundColor: "rgba(19, 112, 255, 1)",
border: "none",
left: 12,
},
},
/** 样式 */
style: {
padding: "20px",
},
},
/** 坐标 */
position: {
x: Number(item.position?.x) || 0,
y: Number(item.position?.y) || 0,
},
/**
* extent: "parent" 跟随父节点移动
* draggable: false 节点不可移动
*/
...(item.type === "BATCH"
? { style: { zIndex: -1 }, extent: "parent" }
: { draggable: flowNodeDraggable }),
/** parentNode 父节点ID */
// ...(item.parentNode ? { parentNode: item.parentNode } : {}),
});
});
return val;
}, [tasks, flowType, inSideFlowNodeId, flowNodeDraggable]);
/** 生成初始化的连线节点 */
const initialEdges = useMemo(() => {
const val: ILine[] = [];
tasks?.length &&
tasks.forEach((item) => {
item?.edges?.length &&
item?.edges.forEach((every) => {
const newLine = {
...every,
batchId: item.parentNode ? item.parentNode : item.id,
};
val.push(newLine);
}, []);
});
return val.map((item: ILine) => {
return {
...item,
/** 点击线选中 */
...(selectedEdge?.id === item.id
? {
style: { stroke: "#1370FF", strokeWidth: 2 },
animated: true,
}
: {}),
labelStyle: { fill: "#8A9099" },
labelBgStyle: { fill: "#F7F8FA " },
label: item.label ? `(${item.label})` : "",
};
});
}, [selectedEdge?.id, tasks]);
/** 设置nodeId方法 */
// const setNodeIdFun = useCallback(
// (id: string) => {
// setInSideFlowNodeId("");
// document.getElementById(`point${id}`)?.scrollIntoView(true);
// },
// []
// );
/** flowNode点击事件 */
const onNodeClick = (e: any, node: Node) => {
console.log(tasks);
console.log(node);
tasks?.forEach((item) => {
if (item.id === node.id) {
setInSideFlowNodeId(node.id);
}
});
if (onFlowNodeClick) {
onFlowNodeClick(node.id);
}
/** 点击node统一清除选中的edge */
setSelectedEdge(undefined);
};
// 点击面板、画布
const handlePaneClick = () => {
setInSideFlowNodeId("");
setSelectedEdge(undefined);
};
/** node节点 */
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
/** 连线数组 */
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
useEffect(() => {
setEdges(initialEdges);
}, [initialEdges, setEdges]);
useEffect(() => {
setNodes(initialNodes);
}, [initialNodes, setNodes]);
/** 节点拖动停止 */
const onNodeDragStop = useCallback(
(event: React.MouseEvent, node: Node) => {
const newVal =
(tasks?.length &&
tasks.map((item) => {
if (item.id === node.id) {
return {
...item,
position: node.position,
};
} else {
return item;
}
})) ||
[];
setTasks && setTasks(newVal);
},
[setTasks, tasks]
);
const connectModifyParameters = useCallback(
(parameters: IParameter[], edgeItem: Connection) => {
return parameters.map((item) => {
if (item.name === edgeItem.targetHandle) {
const { error, helperText } = getCustomTemplateParameterCheckResult({
...item,
linked: true,
hidden: true,
});
return { ...item, linked: true, hidden: true, helperText, error };
} else {
return item;
}
});
},
[]
);
/** 获取连接线的端点类型 */
const getClassType = useCallback(
(connection: Connection) => {
let inputClassType = "",
outClassType: string | undefined = undefined;
tasks?.length &&
tasks.forEach((item) => {
if ([connection.source, connection.target].includes(item.id)) {
item.parameters.forEach((every) => {
if (every.name === connection.targetHandle) {
inputClassType = every.classType;
}
if (every.name === connection.sourceHandle) {
outClassType = every.classType;
}
});
}
});
return { inputClassType, outClassType };
},
[tasks]
);
/** 连接校验并修改值 */
const connectCheck = useCallback(
(connection: Connection) => {
const newVal =
(tasks?.length &&
tasks?.map((item) => {
if (item.id === connection.source) {
return {
...item,
edges: [
...item.edges,
{
...connection,
id: uuid(),
},
],
};
} else if (item.id === connection.target) {
return {
...item,
parameters: connectModifyParameters(
item.parameters,
connection
),
};
} else {
return item;
}
})) ||
[];
return newVal;
},
[connectModifyParameters, tasks]
);
/** 已经连接线啦 */
const onConnect = useCallback(
(connection: Connection) => {
const { inputClassType, outClassType } = getClassType(connection);
let result: ITask[] = [];
if (inputClassType === outClassType) {
result = connectCheck(connection) as ITask[];
} else {
Message.error("端口数据类型不一致,无法连接!");
result = tasksDeleteLine(connection);
}
setTasks && setTasks(result);
},
[Message, connectCheck, getClassType, setTasks, tasksDeleteLine]
);
/** 点击连线 */
const onEdgeClick = useCallback((e: any, val: Edge) => {
setSelectedEdge(val);
/** 点击连线清除选中的node ID */
setInSideFlowNodeId("");
}, []);
const reactFlowParams =
flowType === "edit"
? {
onNodesChange,
onEdgesChange,
onNodeDragStop,
onConnect,
onEdgeClick,
}
: {};
return (
<ReactFlow
className={styles.reactFlowBox}
nodes={nodes}
edges={edges}
fitView={flowType === "default" ? true : false}
{...reactFlowParams}
// proOptions={{ hideAttribution: true, account: "" }}
nodeTypes={nodeTypes}
onPaneClick={handlePaneClick}
onNodeClick={onNodeClick}
{...other}
>
{showControls && <Controls />}
<Background color="#aaa" gap={16} />
</ReactFlow>
);
};
export default BatchOperatorFlow;
......@@ -8,11 +8,15 @@
*/
import classNames from "classnames";
import { Handle, Position } from "react-flow-renderer";
import { useMemo } from "react";
// import { IParameter } from "@/views/Project/ProjectSubmitWork/interface";
import { uuid } from "@/utils/util";
import { IExecutionStatus } from "@/views/Project/ProjectSubmitWork/interface";
import jobFail from "@/assets/project/jobFail.svg";
import jobRun from "@/assets/project/jobRun.svg";
import jobSue from "@/assets/project/jobSue.svg";
import MyTooltip from "@/components/mui/MyTooltip";
import styles from "./index.module.css";
/** 自定义flow节点 */
......@@ -35,23 +39,65 @@ const FlowNode = (props: any) => {
const {
dotStatus,
selectedStatus,
info: { title, isCheck, executionStatus },
flowNodeStyle = { display: "flex", alignItems: "center" }, // 样式
inStyle = { background: "#C2C6CC ", left: 12 }, // 样式
outStyle = { background: "#C2C6CC ", left: 12 }, // 样式
info: { title, isCheck, executionStatus, parameters },
} = data;
/** 获取输入参数数组 */
const inParamsArr = useMemo(() => {
return (
(parameters?.length &&
parameters?.filter((item: any) => {
return item.parameterGroup === "in";
})) ||
[]
);
}, [parameters]);
/** 获取输出参数数组 */
const outParamsArr = useMemo(() => {
return (
(parameters?.length &&
parameters?.filter((item: any) => {
return item.parameterGroup === "out";
})) ||
[]
);
}, [parameters]);
return (
<div
style={flowNodeStyle}
className={classNames({
[styles.flowNode]: true,
[styles.selectedFlowBox]: selectedStatus,
})}
>
{dotStatus?.isInput ? (
<Handle
style={{ background: "#C2C6CC ", left: 12 }}
type="target"
position={Position.Top}
/>
<Handle style={inStyle} type="target" position={Position.Top} />
) : null}
<div style={{ display: "flex", alignItems: "center" }}>
{inParamsArr?.length
? inParamsArr.map((item: any, index: number) => {
return (
<MyTooltip title={item.name} key={uuid()}>
<Handle
id={item.name}
style={{
background: "#fff ",
border: "1px solid #D1D6DE",
left: index * 20 + 20,
...inStyle,
}}
type="target"
position={Position.Top}
/>
</MyTooltip>
);
})
: null}
<div>
{title || ""}
{isCheck && <span className={styles.successDot}></span>}
{getImgUrl(executionStatus) && (
......@@ -63,12 +109,27 @@ const FlowNode = (props: any) => {
)}
</div>
{dotStatus?.isOutput ? (
<Handle
style={{ background: "#C2C6CC ", left: 12 }}
type="source"
position={Position.Bottom}
/>
<Handle style={outStyle} type="source" position={Position.Bottom} />
) : null}
{outParamsArr?.length
? outParamsArr.map((item: any, index: number) => {
return (
<MyTooltip title={item.name} key={uuid()}>
<Handle
id={item.name}
style={{
background: "#fff ",
border: "1px solid #D1D6DE",
left: index * 20 + 20,
...outStyle,
}}
type="source"
position={Position.Bottom}
/>
</MyTooltip>
);
})
: null}
</div>
);
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment