Commit 93b21ec8 authored by chenshouchao's avatar chenshouchao

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

parent 86b7b820
...@@ -41,6 +41,8 @@ const RESTAPI = { ...@@ -41,6 +41,8 @@ const RESTAPI = {
API_SAVE_USERSPEC:`${BACKEND_API_URI_PREFIX}/cpp/workflow/saveuserspec`, // 保存用户自定义工作流模板 API_SAVE_USERSPEC:`${BACKEND_API_URI_PREFIX}/cpp/workflow/saveuserspec`, // 保存用户自定义工作流模板
API_OVERVIEW_GET:`${BACKEND_API_URI_PREFIX}/cpp/basicInformation`, // 获取概览基本信息 API_OVERVIEW_GET:`${BACKEND_API_URI_PREFIX}/cpp/basicInformation`, // 获取概览基本信息
API_TASK_OVERVIEW_LIST:`${BACKEND_API_URI_PREFIX}/cpp/workflowJobInformation`, // 查询任务概览 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; export default RESTAPI;
...@@ -270,6 +270,44 @@ const getTaskOverview=(params:getTaskOverviewParams)=>{ ...@@ -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 { export {
current, current,
...@@ -292,5 +330,7 @@ export { ...@@ -292,5 +330,7 @@ export {
submitWorkFlow, submitWorkFlow,
getworkFlowTaskInfo, getworkFlowTaskInfo,
getOverviewInfo, getOverviewInfo,
getTaskOverview getTaskOverview,
getOperatorList,
saveBatchActor
}; };
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
align-items: center; align-items: center;
border: 1px solid #e6e8eb; border: 1px solid #e6e8eb;
border-radius: 4px; border-radius: 4px;
background-color: #F0F2F5; background-color: #f0f2f5;
cursor: pointer; cursor: pointer;
height: 32px; height: 32px;
box-sizing: border-box; box-sizing: border-box;
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
} }
.radio { .radio {
position: relative; position: relative;
min-width: 64px; min-width: 63px;
height: 28px; height: 28px;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
...@@ -28,18 +28,8 @@ ...@@ -28,18 +28,8 @@
white-space: nowrap; white-space: nowrap;
} }
.radio:not(:last-child)::before {
position: absolute;
width: 1px;
height: 16px;
top: 6px;
right: 0;
content: '';
background-color: #D1D6DE;
}
.radioActive { .radioActive {
color: #1370ff; color: #1370ff;
background-color: #fff; 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 @@ ...@@ -38,22 +38,36 @@
} }
.li { .li {
background-color: RGBA(240, 242, 245, 1); background-color: RGBA(240, 242, 245, 1);
padding: 7px 7px 7px 28px; padding: 7px 9px;
color: rgba(30, 38, 51, 1); color: rgba(30, 38, 51, 1);
font-size: 14px; font-size: 14px;
line-height: 22px; line-height: 22px;
margin-bottom: 12px; margin-bottom: 12px;
position: relative; 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 { .name {
margin-right: 8px; margin-right: 8px;
} }
.version {
white-space: nowrap;
}
.icon { .icon {
width: 6px; width: 6px;
height: 10px; height: 10px;
position: absolute; /* position: absolute;
top: 13px; top: 13px;
left: 9px; left: 9px; */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
......
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"; ...@@ -3,7 +3,15 @@ import { observer } from "mobx-react-lite";
import FullScreenDrawer from "@/components/CommonComponents/FullScreenDrawer"; import FullScreenDrawer from "@/components/CommonComponents/FullScreenDrawer";
import MyButton from "@/components/mui/MyButton"; import MyButton from "@/components/mui/MyButton";
import OperatorList from "./components/OperatorList"; 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"; import styles from "./index.module.css";
type IProps = { type IProps = {
...@@ -12,24 +20,162 @@ type IProps = { ...@@ -12,24 +20,162 @@ type IProps = {
const CustomOperator = observer((props: IProps) => { const CustomOperator = observer((props: IProps) => {
const { setShowCustomOperator } = props; 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 [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 ( return (
<FullScreenDrawer handleClose={setShowCustomOperator} zIndex={1100}> <FullScreenDrawer handleClose={setShowCustomOperator} zIndex={1100}>
<div className={styles.customOperator}> <div className={styles.customOperator}>
<div className={styles.coTop}> <div className={styles.coTop}>
<div className={styles.coTitle}>添加算子</div> <div className={styles.coTitle}>添加算子</div>
<MyButton text="添加"></MyButton> <MyButton
text="添加"
onClick={() => {
handleCheck();
}}
></MyButton>
</div> </div>
<div className={styles.coContent}> <div className={styles.coContent} id="customOperatorFlow">
<OperatorList /> <OperatorList
<Flow operatorList={operatorList}
showControls={false} setOperatorList={setOperatorList}
// tasks={templateConfigInfo} />
// setTasks={setTemplateConfigInfo} <BatchOperatorFlow
tasks={operatorList}
setTasks={setOperatorList}
type="edit" type="edit"
// onFlowNodeClick={handleNodeClick} onFlowNodeClick={handleNodeClick}
flowNodeDraggable={true}
// ListenState={!saveFormDialog} // ListenState={!saveFormDialog}
showControls={false}
/> />
</div> </div>
</div> </div>
......
...@@ -8,11 +8,15 @@ ...@@ -8,11 +8,15 @@
*/ */
import classNames from "classnames"; import classNames from "classnames";
import { Handle, Position } from "react-flow-renderer"; 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 { IExecutionStatus } from "@/views/Project/ProjectSubmitWork/interface";
import jobFail from "@/assets/project/jobFail.svg"; import jobFail from "@/assets/project/jobFail.svg";
import jobRun from "@/assets/project/jobRun.svg"; import jobRun from "@/assets/project/jobRun.svg";
import jobSue from "@/assets/project/jobSue.svg"; import jobSue from "@/assets/project/jobSue.svg";
import MyTooltip from "@/components/mui/MyTooltip";
import styles from "./index.module.css"; import styles from "./index.module.css";
/** 自定义flow节点 */ /** 自定义flow节点 */
...@@ -35,23 +39,65 @@ const FlowNode = (props: any) => { ...@@ -35,23 +39,65 @@ const FlowNode = (props: any) => {
const { const {
dotStatus, dotStatus,
selectedStatus, 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; } = 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 ( return (
<div <div
style={flowNodeStyle}
className={classNames({ className={classNames({
[styles.flowNode]: true, [styles.flowNode]: true,
[styles.selectedFlowBox]: selectedStatus, [styles.selectedFlowBox]: selectedStatus,
})} })}
> >
{dotStatus?.isInput ? ( {dotStatus?.isInput ? (
<Handle style={inStyle} type="target" position={Position.Top} />
) : null}
{inParamsArr?.length
? inParamsArr.map((item: any, index: number) => {
return (
<MyTooltip title={item.name} key={uuid()}>
<Handle <Handle
style={{ background: "#C2C6CC ", left: 12 }} id={item.name}
style={{
background: "#fff ",
border: "1px solid #D1D6DE",
left: index * 20 + 20,
...inStyle,
}}
type="target" type="target"
position={Position.Top} position={Position.Top}
/> />
) : null} </MyTooltip>
<div style={{ display: "flex", alignItems: "center" }}> );
})
: null}
<div>
{title || ""} {title || ""}
{isCheck && <span className={styles.successDot}></span>} {isCheck && <span className={styles.successDot}></span>}
{getImgUrl(executionStatus) && ( {getImgUrl(executionStatus) && (
...@@ -63,12 +109,27 @@ const FlowNode = (props: any) => { ...@@ -63,12 +109,27 @@ const FlowNode = (props: any) => {
)} )}
</div> </div>
{dotStatus?.isOutput ? ( {dotStatus?.isOutput ? (
<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 <Handle
style={{ background: "#C2C6CC ", left: 12 }} id={item.name}
style={{
background: "#fff ",
border: "1px solid #D1D6DE",
left: index * 20 + 20,
...outStyle,
}}
type="source" type="source"
position={Position.Bottom} position={Position.Bottom}
/> />
) : null} </MyTooltip>
);
})
: null}
</div> </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