Commit 4ba0aef4 authored by wuyongsheng's avatar wuyongsheng

Merge branch 'feat-20220705-customTemplate' into 'release'

Feat 20220705 custom template merge release cn-

See merge request sunyihao/bkunyun!91
parents 2c85ceda f474ad84
......@@ -2,7 +2,7 @@
* @Author: 吴永生#A02208 yongsheng.wu@wholion.com
* @Date: 2022-06-13 09:56:57
* @LastEditors: 吴永生#A02208 yongsheng.wu@wholion.com
* @LastEditTime: 2022-06-24 16:32:31
* @LastEditTime: 2022-07-07 18:19:20
* @FilePath: /bkunyun/src/api/api_manager.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
......@@ -36,6 +36,7 @@ const RESTAPI = {
API_WORKBENCH_CANCEL_WORKFLOWJOB: `${BACKEND_API_URI_PREFIX}/cpp/workflow/cancel`, //取消工作流
API_SUBMIT_WORKFLOW: `${BACKEND_API_URI_PREFIX}/cpp/workflow/submit`, //提交工作流
API_WORKBENCH_WORKFLOW_TASKINFO: `${BACKEND_API_URI_PREFIX}/cpp/workbench/workflowjob/task-info`, //查询任务某个算子详情
API_OPERATOR_LIST:`${BACKEND_API_URI_PREFIX}/cpp/workflow/actorspecs`, // 获取算子列表
};
export default RESTAPI;
export interface IGetOperatorList {
owner: string;
productId: string;
keyword?: string
}
\ No newline at end of file
/*
* @Author: 吴永生#A02208 yongsheng.wu@wholion.com
* @Date: 2022-07-05 14:00:37
* @LastEditors: 吴永生#A02208 yongsheng.wu@wholion.com
* @LastEditTime: 2022-07-07 18:24:16
* @FilePath: /bkunyun/src/api/workbench_api.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import request from "@/utils/axios/service";
import Api from "./api_manager";
import { IGetOperatorList } from "./workbenchInterface";
function current() {
return request({
......@@ -119,6 +128,15 @@ const cancelWorkflowJob = (params: workflowJobCancelParams) => {
});
};
// 获取算子列表数据
const fetchOperatorList = (params: IGetOperatorList) => {
return request({
url: Api.API_OPERATOR_LIST,
method: "get",
params,
});
};
export {
current,
menu,
......@@ -128,5 +146,6 @@ export {
addWorkbenchTemplate,
getWorkflowJobList,
deleteWorkflowJob,
cancelWorkflowJob
cancelWorkflowJob,
fetchOperatorList
};
.RadiosBox {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #e6e8eb;
border-radius: 4px;
background-color: #e6e8eb;
cursor: pointer;
height: 32px;
box-sizing: border-box;
padding: 2px;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #e6e8eb;
border-radius: 4px;
background-color: #e6e8eb;
cursor: pointer;
height: 32px;
box-sizing: border-box;
padding: 2px;
}
.radio {
height: 28px;
box-sizing: border-box;
font-size: 14px;
color: #565c66;
border-radius: 4px;
line-height: 20px;
padding: 3px 18px;
background-color: #e6e8eb;
display: flex;
align-items: center;
height: 28px;
box-sizing: border-box;
font-size: 14px;
color: #565c66;
border-radius: 4px;
line-height: 20px;
padding: 3px 18px;
background-color: #e6e8eb;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
}
.radioActive {
color: #1370ff;
background-color: #fff;
border: 1px solid #e6e8eb;
color: #1370ff;
background-color: #fff;
border: 1px solid #e6e8eb;
}
......@@ -3,37 +3,41 @@ import classnames from "classnames";
import style from "./index.module.css";
type radioOption = {
value: string;
label: string;
value: string;
label: string;
};
type IRadioGroupOfButtonStyleProps = {
radioOptions: Array<radioOption>;
value: string;
handleRadio: any;
radioOptions: Array<radioOption>;
value: string;
handleRadio: any;
RadiosBoxStyle?: object;
radioStyle?: object;
};
const RadioGroupOfButtonStyle = (props: IRadioGroupOfButtonStyleProps) => {
const { radioOptions, value, handleRadio } = props;
const { radioOptions, value, handleRadio, RadiosBoxStyle, radioStyle } =
props;
return (
<div className={style.RadiosBox}>
{radioOptions.map((options) => {
return (
<div
key={options.value}
className={classnames({
[style.radio]: true,
[style.radioActive]: value === options.value,
})}
onClick={() => handleRadio(options.value)}
>
{options.label}
</div>
);
})}
</div>
);
return (
<div className={style.RadiosBox} style={RadiosBoxStyle}>
{radioOptions.map((options) => {
return (
<div
key={options.value}
className={classnames({
[style.radio]: true,
[style.radioActive]: value === options.value,
})}
onClick={() => handleRadio(options.value)}
style={radioStyle}
>
{options.label}
</div>
);
})}
</div>
);
};
export default RadioGroupOfButtonStyle;
import * as React from "react";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import CheckIcon from "@mui/icons-material/Check";
import { ThemeProvider, createTheme } from "@mui/material/styles";
type IOption = {
label: string;
value: string;
};
type IMyMenuProps = {
children: React.ReactNode;
options: Array<IOption>;
value: string;
setValue?: any;
};
const theme = createTheme({
components: {
MuiMenu: {
styleOverrides: {
root: {
maxHeight: "260px",
overflowY: "scroll",
},
},
},
MuiMenuItem: {
styleOverrides: {
root: {
fontSize: "14px",
lineHeight: "36px",
display: "flex",
justifyContent: "space-between",
":hover": {
color: "rgba(19, 112, 255, 1)",
},
"&.Mui-selected": {
backgroundColor: "#fff",
color: "rgba(19, 112, 255, 1)",
},
},
},
},
MuiSvgIcon: {
styleOverrides: {
root: {
width: "16px",
height: "16px",
},
},
},
},
});
const MyMenu = (props: IMyMenuProps) => {
const { children, options, value, setValue } = props;
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = (value: string) => {
setAnchorEl(null);
setValue && setValue(value);
};
return (
<ThemeProvider theme={theme}>
<div>
<div onClick={handleClick}>{children}</div>
<Menu
id="basic-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
"aria-labelledby": "basic-button",
}}
>
{options.map((option, index) => {
return (
<MenuItem
onClick={() => handleClose(option.value)}
selected={value === option.value}
key={index}
>
<span>{option.label}</span>
{value === option.value && <CheckIcon />}
</MenuItem>
);
})}
</Menu>
</div>
</ThemeProvider>
);
};
export default MyMenu;
......@@ -15,87 +15,91 @@ import { TabContext, TabList, TabPanel } from "@mui/lab";
import { Typography } from "@mui/material";
interface ITabList {
label: string;
value: string;
component: JSX.Element;
icon?: string;
iconed?: string;
hide?: boolean;
label: string;
value: string;
component: JSX.Element;
icon?: string;
iconed?: string;
hide?: boolean;
}
interface IProps {
tabList: ITabList[];
defaultValue?: string;
tabList: ITabList[];
defaultValue?: string;
}
const Tabs = (props: IProps) => {
const { tabList, defaultValue } = props;
const [value, setValue] = useState(defaultValue || tabList.filter(e => !e.hide)[0].value);
const { tabList, defaultValue } = props;
const [value, setValue] = useState(
defaultValue || tabList.filter((e) => !e.hide)[0].value
);
const onChange = (val: string) => {
setValue(val);
};
const onChange = (val: string) => {
setValue(val);
};
const labelRender = (item: ITabList, key: number) => {
return (
<Box style={{ display: "flex", alignItems: "center" }}>
{item.icon ? (
<img
style={{ width: "14px", marginRight: "10px" }}
src={value === item.value ? item.iconed : item.icon}
alt=""
/>
) : (
""
)}
<Typography sx={{ fontSize: "14px", fontWeight: "400" }}>
{item.label}
</Typography>
</Box>
);
};
const labelRender = (item: ITabList, key: number) => {
return (
<Box style={{ display: "flex", alignItems: "center" }}>
{item.icon ? (
<img
style={{ width: "14px", marginRight: "10px" }}
src={value === item.value ? item.iconed : item.icon}
alt=""
/>
) : (
""
)}
<Typography sx={{ fontSize: "14px", fontWeight: "400" }}>
{item.label}
</Typography>
</Box>
);
};
return (
<TabContext value={value}>
<Box sx={{ borderBottom: 1, borderColor: "#F0F2F5" }}>
<TabList
onChange={(e: any, val: string) => {
onChange(val);
}}
>
{tabList?.map((item, key) => {
if (item.hide) return "";
return (
<Tab
key={key}
label={labelRender(item, key)}
value={item.value}
id={item.value}
/>
);
})}
</TabList>
</Box>
{tabList?.map((item) => {
return (
<TabPanel
sx={{ padding: "20px 0 0 0" }}
value={item.value}
key={item.value}
>
{item.component}
</TabPanel>
);
})}
</TabContext>
);
return (
<TabContext value={value}>
<Box sx={{ borderBottom: 1, borderColor: "#F0F2F5" }}>
<TabList
onChange={(e: any, val: string) => {
onChange(val);
}}
>
{tabList
?.filter((item) => !item.hide)
.map((item, key) => {
return (
<Tab
key={key}
label={labelRender(item, key)}
value={item.value}
id={item.value}
/>
);
})}
</TabList>
</Box>
{tabList
?.filter((item) => !item.hide)
.map((item) => {
return (
<TabPanel
sx={{ padding: "20px 0 0 0" }}
value={item.value}
key={item.value}
>
{item.component}
</TabPanel>
);
})}
</TabContext>
);
};
const handleEqual = (prvProps: IProps, nextProps: IProps) => {
if (isEqual(prvProps, nextProps)) {
return true;
}
return false;
if (isEqual(prvProps, nextProps)) {
return true;
}
return false;
};
export default memo(Tabs, handleEqual);
......@@ -2,7 +2,7 @@
* @Author: 吴永生#A02208 yongsheng.wu@wholion.com
* @Date: 2022-05-31 10:17:23
* @LastEditors: 吴永生#A02208 yongsheng.wu@wholion.com
* @LastEditTime: 2022-06-14 10:28:43
* @LastEditTime: 2022-07-07 22:03:00
* @FilePath: /bkunyun/src/react-app-env.d.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
......@@ -80,7 +80,9 @@ declare module "*.module.sass" {
declare module "@mui/lab";
declare module "lodash";
declare module 'lodash/cloneDeep'
declare module "@mui/material/Tab";
declare module "tus-js-client";
declare module "uuid";
......@@ -73,22 +73,22 @@ const ProjectSubmitWork = observer(() => {
const { name, state } = workFlowJobInfo || {};
/** 获取模版数据 */
const { run } = useMyRequest(fetchWorkFlowJob, {
pollingInterval: 1000 * 60,
pollingWhenHidden: false,
onSuccess: (res: IResponse<ITaskInfo>) => {
getOutouts(res.data.outputs);
setWorkFlowJobInfo(res.data);
},
});
/** 获取模版数据 */
const { run } = useMyRequest(fetchWorkFlowJob, {
pollingInterval: 1000 * 60,
pollingWhenHidden: false,
onSuccess: (res: IResponse<ITaskInfo>) => {
getOutouts(res.data.outputs);
setWorkFlowJobInfo(res.data);
},
});
useEffect(() => {
const locationInfo: any = location?.state;
run({
id: locationInfo.taskId,
});
}, [location?.state, run]);
useEffect(() => {
const locationInfo: any = location?.state;
run({
id: locationInfo.taskId,
});
}, [location?.state, run]);
const { run: getworkFlowTaskInfoRun } = useMyRequest(getworkFlowTaskInfo, {
onSuccess: (res) => {
......@@ -96,34 +96,35 @@ const ProjectSubmitWork = observer(() => {
},
});
const goToProjectData = (path: string) => {
path = path.slice(13);
if (path) {
navigate(`/product/cadd/projectData`, {
state: { pathName: path },
});
} else {
navigate(`/product/cadd/projectData`, {
state: { pathName: "/" },
});
}
};
// 前往数据管理页面
const goToProjectData = (path: string) => {
path = path.slice(13);
if (path) {
navigate(`/product/cadd/projectData`, {
state: { pathName: path },
});
} else {
navigate(`/product/cadd/projectData`, {
state: { pathName: "/" },
});
}
};
// 通过文件路径获取文件所在文件夹路径 如 输入 /home/cloudam/task_a.out 输出/home/cloudam/
const getFolderPath = (path: string) => {
const lastIndex = path.lastIndexOf("/");
if (lastIndex !== -1) {
path = path.slice(0, lastIndex + 1);
}
return path;
};
// 通过文件路径获取文件所在文件夹路径 如 输入 /home/cloudam/task_a.out 输出/home/cloudam/
const getFolderPath = (path: string) => {
const lastIndex = path.lastIndexOf("/");
if (lastIndex !== -1) {
path = path.slice(0, lastIndex + 1);
}
return path;
};
/** 返回事件 */
const onBack = useCallback(() => {
navigate("/product/cadd/projectWorkbench", {
state: { type: "workbenchList" },
});
}, [navigate]);
/** 返回事件 */
const onBack = useCallback(() => {
navigate("/product/cadd/projectWorkbench", {
state: { type: "workbenchList" },
});
}, [navigate]);
const outputPathTransform = (path: string) => {
path = path.slice(13);
......@@ -339,7 +340,9 @@ const ProjectSubmitWork = observer(() => {
<div key={index} className={styles.outputLi}>
<MyPopconfirm
title="即将跳转至项目数据内该任务的结果目录,确认继续吗?"
onConfirm={() => goToProjectData(getFolderPath(item.path))}
onConfirm={() =>
goToProjectData(getFolderPath(item.path))
}
>
<div className={styles.outputLiLeft}>
<img
......@@ -510,16 +513,16 @@ const ProjectSubmitWork = observer(() => {
{patchInfo?.children.map((item: any, index: number) => {
return (
<div
key={index}
className={classNames({
[styles.option]: true,
[styles.optionActive]:
activeFlowIndex === index,
})}
onClick={() => setActiveFlowIndex(index)}
>
{item.title}
</div>
key={index}
className={classNames({
[styles.option]: true,
[styles.optionActive]:
activeFlowIndex === index,
})}
onClick={() => setActiveFlowIndex(index)}
>
{item.title}
</div>
);
})}
</div>
......
......@@ -2,7 +2,7 @@
* @Author: 吴永生#A02208 yongsheng.wu@wholion.com
* @Date: 2022-05-31 10:18:13
* @LastEditors: 吴永生#A02208 yongsheng.wu@wholion.com
* @LastEditTime: 2022-06-16 20:49:14
* @LastEditTime: 2022-07-07 18:08:33
* @FilePath: /bkunyun/src/views/Project/ProjectSetting/index.tsx
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
......@@ -28,168 +28,176 @@ import { isProjectOwner } from "@/utils/util";
import { observer } from "mobx-react";
const ProjectMembers = observer(() => {
const http = useHttp();
const http = useHttp();
const { currentProjectStore } = useStores();
const { currentProjectStore } = useStores();
/** 删除成员 */
const [removeDialog, setRemoveDialog] = useState<IDialogInfo>({
isShow: false,
username: "",
});
/** 更改权限 */
const [permissionDialog, setPermissionDialog] = useState<IDialogInfo>({
isShow: false,
username: "",
});
/** 添加成员 */
const [addMemberDialog, setAddMemberDialog] = useState<boolean>(false);
/** 表格数据 */
const [tableData, setTableData] = useState([]);
/** 项目名称 */
const [projectName, setProjectMember] = useState("");
/** 过滤后数据 */
const [filterTableData, setFilterTableData] = useState([]);
/** 删除成员 */
const [removeDialog, setRemoveDialog] = useState<IDialogInfo>({
isShow: false,
username: "",
});
/** 更改权限 */
const [permissionDialog, setPermissionDialog] = useState<IDialogInfo>({
isShow: false,
username: "",
});
/** 添加成员 */
const [addMemberDialog, setAddMemberDialog] = useState<boolean>(false);
/** 表格数据 */
const [tableData, setTableData] = useState([]);
/** 当前项目用户权限 */
const [projectRole, setProjectRole] = useState<string>("");
/** 项目名称 */
const [projectName, setProjectMember] = useState("");
/** 过滤后数据 */
const [filterTableData, setFilterTableData] = useState([]);
const columns = useMemo(() => {
const val: any = [
{ id: "username", label: "成员名称" },
{ id: "projectRoleDesc", label: "项目权限" },
{ id: "phone", label: "联系方式" },
{
id: "operation",
label: "操作",
width: 160,
render: (item: any, row: any) => {
return row?.projectRole === "OWNER" ? null : (
<>
<span
style={{ color: "#1370FF", cursor: "pointer" }}
onClick={() => {
onPermissionBtn(row);
}}
>
更改权限
</span>
<span
className={styles.removeItemBox}
onClick={() => {
onRemoveItemBtn(row.username);
}}
>
移出项目
</span>
</>
);
},
},
];
return val;
}, []);
const columns = useMemo(() => {
const val: any = [
{ id: "username", label: "成员名称" },
{ id: "projectRoleDesc", label: "项目权限" },
{ id: "phone", label: "联系方式" },
...(projectRole !== "OWNER"
? []
: [
{
id: "operation",
label: "操作",
width: 160,
render: (item: any, row: any) => {
return row?.projectRole === "OWNER" ? null : (
<>
<span
style={{ color: "#1370FF", cursor: "pointer" }}
onClick={() => {
onPermissionBtn(row);
}}
>
更改权限
</span>
<span
className={styles.removeItemBox}
onClick={() => {
onRemoveItemBtn(row.username);
}}
>
移出项目
</span>
</>
);
},
},
]),
];
return val;
}, [projectRole]);
/** 获取表格数据 */
const getTableList = useCallback(() => {
const projectInfo = toJS(currentProjectStore?.currentProjectInfo);
if (!projectInfo?.id) return;
http
.get<IResponse<any>>("/cpp/project/get", {
params: { id: projectInfo?.id || "" },
})
.then((res) => {
const { data = {} } = res;
setTableData(data?.members || []);
});
}, [currentProjectStore?.currentProjectInfo, http]);
/** 获取表格数据 */
const getTableList = useCallback(() => {
const projectInfo = toJS(currentProjectStore?.currentProjectInfo);
if (!projectInfo?.id) return;
http
.get<IResponse<any>>("/cpp/project/get", {
params: { id: projectInfo?.id || "" },
})
.then((res) => {
const { data = {} } = res;
setTableData(data?.members || []);
console.log(data?.projectRole, data?.projectRole);
setProjectRole(data?.projectRole || "");
});
}, [currentProjectStore?.currentProjectInfo, http]);
useEffect(() => {
getTableList();
}, [getTableList]);
useEffect(() => {
getTableList();
}, [getTableList]);
useEffect(() => {
if (!!projectName) {
const newVal =
tableData.filter((item: any) => {
return item?.username?.includes(projectName);
}) || [];
setFilterTableData(newVal || []);
} else {
setFilterTableData(tableData);
}
}, [projectName, tableData]);
useEffect(() => {
if (!!projectName) {
const newVal =
tableData.filter((item: any) => {
return item?.username?.includes(projectName);
}) || [];
setFilterTableData(newVal || []);
} else {
setFilterTableData(tableData);
}
}, [projectName, tableData]);
/** 点击添加成员 */
const onAddMember = () => {
setAddMemberDialog(true);
};
/** 点击添加成员 */
const onAddMember = () => {
setAddMemberDialog(true);
};
/** 点击删除成员 */
const onRemoveItemBtn = (userName: string) => {
setRemoveDialog({ isShow: true, username: userName });
};
/** 点击删除成员 */
const onRemoveItemBtn = (userName: string) => {
setRemoveDialog({ isShow: true, username: userName });
};
/** 点击更改权限 */
const onPermissionBtn = (row: any) => {
setPermissionDialog({
isShow: true,
username: row?.username || "",
projectRole: row?.projectRole || "",
});
};
/** 点击更改权限 */
const onPermissionBtn = (row: any) => {
setPermissionDialog({
isShow: true,
username: row?.username || "",
projectRole: row?.projectRole || "",
});
};
return (
<>
<Box className={styles.headerBox}>
<OutlinedInput
onChange={(e: any) => {
_.debounce(() => {
setProjectMember(e.target.value);
}, 200)();
}}
placeholder="搜索项目成员"
size="small"
sx={{ width: 340, height: 32 }}
endAdornment={<SearchIcon style={{ color: "#8A9099" }} />}
/>
{currentProjectStore?.currentProjectInfo?.projectRole === "OWNER" ? (
<Button
style={{ backgroundColor: "#1370FF " }}
variant="contained"
onClick={onAddMember}
startIcon={<Add />}
size="small"
>
添加成员
</Button>
) : null}
</Box>
<Table
rowHover={true}
stickyheader={true}
rows={filterTableData}
rowsPerPage={"99"}
headCells={columns}
nopadding={true}
footer={false}
tableStyle={{ minWidth: "auto" }}
borderBottom={"none"}
/>
<RemoveItem
removeDialog={removeDialog}
setRemoveDialog={setRemoveDialog}
getTableList={getTableList}
/>
<ChangePermission
permissionDialog={permissionDialog}
getTableList={getTableList}
setPermissionDialog={setPermissionDialog}
/>
<AddMember
addMemberDialog={addMemberDialog}
setAddMemberDialog={setAddMemberDialog}
getTableList={getTableList}
/>
</>
);
return (
<>
<Box className={styles.headerBox}>
<OutlinedInput
onChange={(e: any) => {
_.debounce(() => {
setProjectMember(e.target.value);
}, 200)();
}}
placeholder="搜索项目成员"
size="small"
sx={{ width: 340, height: 32 }}
endAdornment={<SearchIcon style={{ color: "#8A9099" }} />}
/>
{currentProjectStore?.currentProjectInfo?.projectRole === "OWNER" ? (
<Button
style={{ backgroundColor: "#1370FF " }}
variant="contained"
onClick={onAddMember}
startIcon={<Add />}
size="small"
>
添加成员
</Button>
) : null}
</Box>
<Table
rowHover={true}
stickyheader={true}
rows={filterTableData}
rowsPerPage={"99"}
headCells={columns}
nopadding={true}
footer={false}
tableStyle={{ minWidth: "auto" }}
borderBottom={"none"}
/>
<RemoveItem
removeDialog={removeDialog}
setRemoveDialog={setRemoveDialog}
getTableList={getTableList}
/>
<ChangePermission
permissionDialog={permissionDialog}
getTableList={getTableList}
setPermissionDialog={setPermissionDialog}
/>
<AddMember
addMemberDialog={addMemberDialog}
setAddMemberDialog={setAddMemberDialog}
getTableList={getTableList}
/>
</>
);
});
export default memo(ProjectMembers);
......@@ -18,474 +18,489 @@ import jobSueIcon from "@/assets/project/jobSue.svg";
import { IParameter } from "../interface";
type ConfigFormProps = {
templateConfigInfo?: ITemplateConfig;
setParameter: any;
onRef?: React.Ref<any>;
setSelectedNodeId: (val: string) => void;
templateConfigInfo?: ITemplateConfig;
setParameter: any;
onRef?: React.Ref<any>;
setSelectedNodeId: (val: string) => void;
};
const ConfigForm = (props: ConfigFormProps) => {
const { templateConfigInfo, setParameter,setSelectedNodeId } = props;
const [name, setName] = useState<string>(""); // 任务名称
const { templateConfigInfo, setParameter, setSelectedNodeId } = props;
const [name, setName] = useState<string>(""); // 任务名称
const [nameHelp, setNameHelp] = useState({
error: false,
helperText: "",
});
const [outputPath, setOutputPath] = useState<string>("ProjectData"); // 输出路径
const [outputPathHelp, setOutputPathHelp] = useState({
error: false,
helperText: "",
});
const getNameAndPath = () => {
return {
name,
outputPath,
nameAndOutputPathCheck: !(checkName(name) || checkOutputPath(outputPath)), // 任务名称、输出路径是否通过校验
};
};
const [nameHelp, setNameHelp] = useState({
error: false,
helperText: "",
});
const [outputPath, setOutputPath] = useState<string>("ProjectData"); // 输出路径
const [outputPathHelp, setOutputPathHelp] = useState({
error: false,
helperText: "",
});
const getNameAndPath = () => {
return {
name,
outputPath,
nameAndOutputPathCheck: !(checkName(name) || checkOutputPath(outputPath)), // 任务名称、输出路径是否通过校验
};
};
const setInitName = (name: string) => {
setName(`${name}_${moment(new Date()).format("YYYY_MM_DD_HH_mm")}`);
};
useImperativeHandle(props.onRef, () => {
return {
getNameAndPath: getNameAndPath,
setInitName: setInitName,
};
});
const setInitName = (name: string) => {
setName(`${name}_${moment(new Date()).format("YYYY_MM_DD_HH_mm")}`);
};
useImperativeHandle(props.onRef, () => {
return {
getNameAndPath: getNameAndPath,
setInitName: setInitName,
};
});
const [fileSelectOpen, setFileSelectOpen] = useState(false); // 选择输出路径的弹窗显示控制
const [fileSelectOpen, setFileSelectOpen] = useState(false); // 选择输出路径的弹窗显示控制
const [fileSelectObject, setFileSelectObject] = useState({
taskId: "",
parameterName: "",
});
const [fileSelectObject, setFileSelectObject] = useState({
taskId: "",
parameterName: "",
});
const onFileSelectConfirm = (path: string) => {
setFileSelectOpen(false);
if (fileSelectObject.taskId) {
setParameter(
`ProjectData${path === "/" ? "" : path}`,
fileSelectObject.taskId,
fileSelectObject.parameterName
);
} else {
setOutputPath(`ProjectData${path === "/" ? "" : path}`);
checkOutputPath(`ProjectData${path === "/" ? "" : path}`);
}
};
const onFileSelectConfirm = (path: string) => {
setFileSelectOpen(false);
if (fileSelectObject.taskId) {
setParameter(
`ProjectData${path === "/" ? "" : path}`,
fileSelectObject.taskId,
fileSelectObject.parameterName
);
} else {
setOutputPath(`ProjectData${path === "/" ? "" : path}`);
checkOutputPath(`ProjectData${path === "/" ? "" : path}`);
}
};
const handleFileSelectOnClose = () => {
setFileSelectOpen(false);
};
const handleFileSelectOnClose = () => {
setFileSelectOpen(false);
};
const handleOpenFileSelect = (
taskId: string = "",
parameterName: string = ""
) => {
setFileSelectObject({
taskId,
parameterName,
});
setFileSelectOpen(true);
};
const handleOpenFileSelect = (
taskId: string = "",
parameterName: string = ""
) => {
setFileSelectObject({
taskId,
parameterName,
});
setFileSelectOpen(true);
};
const handleNameChange = (e: any) => {
setName(e.target.value);
checkName(e.target.value);
};
const handleNameChange = (e: any) => {
setName(e.target.value);
checkName(e.target.value);
};
const checkName = (name: string = "") => {
const reg = new RegExp(/^[a-zA-Z0-9\u4e00-\u9fa5-_]{3,30}$/);
if (!name) {
setNameHelp({
error: true,
helperText: "任务名称不能为空",
});
return true;
} else if (reg.test(name)) {
setNameHelp({
error: false,
helperText: "",
});
return false;
} else {
setNameHelp({
error: true,
helperText:
"请输入正确任务名称(3~30字符,可包含大小写字母、数字、中文、特殊符号“-”、“_”)",
});
return true;
}
};
const checkName = (name: string = "") => {
const reg = new RegExp(/^[a-zA-Z0-9\u4e00-\u9fa5-_]{3,30}$/);
if (!name) {
setNameHelp({
error: true,
helperText: "任务名称不能为空",
});
return true;
} else if (reg.test(name)) {
setNameHelp({
error: false,
helperText: "",
});
return false;
} else {
setNameHelp({
error: true,
helperText:
"请输入正确任务名称(3~30字符,可包含大小写字母、数字、中文、特殊符号“-”、“_”)",
});
return true;
}
};
const checkOutputPath = (outputPath: string = "") => {
if (outputPath) {
setOutputPathHelp({
error: false,
helperText: "",
});
return false;
} else {
setOutputPathHelp({
error: true,
helperText: "请选择输出路径",
});
return true;
}
};
const checkOutputPath = (outputPath: string = "") => {
if (outputPath) {
setOutputPathHelp({
error: false,
helperText: "",
});
return false;
} else {
setOutputPathHelp({
error: true,
helperText: "请选择输出路径",
});
return true;
}
};
const renderTasks: IRenderTasks = useMemo(() => {
const result: IRenderTasks = [];
templateConfigInfo?.tasks.forEach((task, taskIndex) => {
if (task.type === "BATCH") {
result.push({ ...task, flows: [], isCheck: true });
} else {
result[result.length - 1]?.flows.push({ ...task });
}
});
result.forEach((task) => {
let isCheck = true;
if (task.parameters.length > 0) {
task.parameters
.filter((parameter) => parameter.hidden === false)
.forEach((parameter) => {
const { error } = getCheckResult(parameter, parameter.value);
if (error) {
isCheck = false;
return;
}
});
}
if (task.flows.length > 0) {
task.flows.forEach((flow) => {
if (flow.parameters.length > 0) {
flow.parameters
.filter((parameter) => parameter.hidden === false)
.forEach((parameter) => {
const { error } = getCheckResult(parameter, parameter.value);
if (error) {
isCheck = false;
return;
}
});
}
});
}
task.isCheck = isCheck;
});
return result;
}, [templateConfigInfo]);
const renderTasks: IRenderTasks = useMemo(() => {
const result: IRenderTasks = [];
templateConfigInfo?.tasks.forEach((task, taskIndex) => {
if (task.type === "BATCH") {
result.push({ ...task, flows: [], isCheck: true });
} else {
result[result.length - 1]?.flows.push({ ...task });
}
});
result.forEach((task) => {
let isCheck = true;
if (task.parameters.length > 0) {
task.parameters
.filter((parameter) => parameter.hidden === false)
.forEach((parameter) => {
const { error } = getCheckResult(parameter, parameter.value);
if (error) {
isCheck = false;
return;
}
});
}
if (task.flows.length > 0) {
task.flows.forEach((flow) => {
if (flow.parameters.length > 0) {
flow.parameters
.filter((parameter) => parameter.hidden === false)
.forEach((parameter) => {
const { error } = getCheckResult(parameter, parameter.value);
if (error) {
isCheck = false;
return;
}
});
}
});
}
task.isCheck = isCheck;
});
return result;
}, [templateConfigInfo]);
const handleParameterChange = (
e: any,
taskId: string,
parameterName: string
) => {
setParameter(e.target.value, taskId, parameterName);
};
const handleParameterChange = (
e: any,
taskId: string,
parameterName: string
) => {
setParameter(e.target.value, taskId, parameterName);
};
const randerParameters = (parameters: Array<IParameter>, taskId: string, batchId?: string) => {
return parameters
.filter((parameter) => parameter.hidden === false)
.map((parameter, parameterIndex) => {
return (
<div
className={styles.parameter}
key={parameter.id || "" + parameterIndex}
>
<div
className={classnames({
[styles.parameterTitle]: true,
[styles.required]: parameter.required,
})}
>
{parameter.name}
<span className={styles.parameterDataType}>
{parameter.classTypeName}
</span>
</div>
<div className={styles.parameterContent}>
{parameter.domType.toLowerCase() === "file" && (
<MyInput
onFocus={()=> setSelectedNodeId(batchId || '')}
onBlur={()=> setSelectedNodeId('')}
value={parameter.value || ""}
InputProps={{
endAdornment: (
<img
onClick={() =>
handleOpenFileSelect(taskId, parameter.name)
}
src={fileSelectIcon}
alt=""
className={styles.fileSelectImg}
/>
),
}}
placeholder="请选择"
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "path" && (
<MyInput
onFocus={()=> setSelectedNodeId(batchId || '')}
onBlur={()=> setSelectedNodeId('')}
value={parameter.value || ""}
InputProps={{
endAdornment: (
<img
onClick={() =>
handleOpenFileSelect(taskId, parameter.name)
}
src={fileSelectIcon}
alt=""
className={styles.fileSelectImg}
/>
),
}}
placeholder="请选择"
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "dataset" && (
<MyInput
onFocus={()=> setSelectedNodeId(taskId)}
onBlur={()=> setSelectedNodeId('')}
value={parameter.value || ""}
InputProps={{
endAdornment: (
<img
onClick={() =>
handleOpenFileSelect(taskId, parameter.name)
}
src={fileSelectIcon}
alt=""
className={styles.fileSelectImg}
/>
),
}}
placeholder="请选择"
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "input" && (
<MyInput
onFocus={()=> {setSelectedNodeId(batchId || ''); console.log(batchId,'111')}}
const randerParameters = (
parameters: Array<IParameter>,
taskId: string,
batchId?: string
) => {
return parameters
.filter((parameter) => parameter.hidden === false)
.map((parameter, parameterIndex) => {
return (
<div
className={styles.parameter}
key={parameter.id || "" + parameterIndex}
>
<div
className={classnames({
[styles.parameterTitle]: true,
[styles.required]: parameter.required,
})}
>
{parameter.name}
<span className={styles.parameterDataType}>
{parameter.classTypeName}
</span>
</div>
<div className={styles.parameterContent}>
{parameter.domType.toLowerCase() === "file" && (
<MyInput
onFocus={() => setSelectedNodeId(batchId || "")}
onBlur={() => setSelectedNodeId("")}
value={parameter.value || ""}
InputProps={{
endAdornment: (
<img
onClick={() =>
handleOpenFileSelect(taskId, parameter.name)
}
src={fileSelectIcon}
alt=""
className={styles.fileSelectImg}
/>
),
}}
placeholder="请选择"
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "path" && (
<MyInput
onFocus={() => setSelectedNodeId(batchId || "")}
onBlur={() => setSelectedNodeId("")}
value={parameter.value || ""}
InputProps={{
endAdornment: (
<img
onClick={() =>
handleOpenFileSelect(taskId, parameter.name)
}
src={fileSelectIcon}
alt=""
className={styles.fileSelectImg}
/>
),
}}
placeholder="请选择"
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "dataset" && (
<MyInput
onFocus={() => setSelectedNodeId(taskId)}
onBlur={() => setSelectedNodeId("")}
value={parameter.value || ""}
InputProps={{
endAdornment: (
<img
onClick={() =>
handleOpenFileSelect(taskId, parameter.name)
}
src={fileSelectIcon}
alt=""
className={styles.fileSelectImg}
/>
),
}}
placeholder="请选择"
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "input" && (
<MyInput
onFocus={() => {
setSelectedNodeId(batchId || "");
console.log(batchId, "111");
}}
onBlur={() => setSelectedNodeId("")}
value={parameter.value || ""}
onChange={(e: any) =>
handleParameterChange(e, taskId, parameter.name || "")
}
placeholder="请输入"
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "select" && (
<MySelect
onFocus={() => setSelectedNodeId(batchId || "")}
onBlur={() => setSelectedNodeId("")}
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(e, taskId, parameter.name || "")
}
error={parameter.error || false}
helpertext={parameter.helperText}
options={optionsTransform(parameter?.choices || [], "label")}
></MySelect>
)}
{parameter.domType.toLowerCase() === "multipleselect" && (
<MySelect
onFocus={() => setSelectedNodeId(batchId || "")}
onBlur={() => setSelectedNodeId("")}
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(e, taskId, parameter.name || "")
}
multiple={true}
error={parameter.error || false}
helpertext={parameter.helperText}
options={optionsTransform(parameter.choices, "label")}
></MySelect>
)}
{parameter.domType.toLowerCase() === "radio" && (
<MyRadio
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(e, taskId, parameter.name || "")
}
onFocus={() => setSelectedNodeId(batchId || "")}
onBlur={() => setSelectedNodeId("")}
options={optionsTransform(parameter.choices, "label")}
error={parameter.error || false}
helperText={parameter.helperText}
></MyRadio>
)}
{parameter.domType.toLowerCase() === "checkbox" && (
<MyCheckBox
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(
{
target: {
value: e,
},
},
taskId,
parameter.name || ""
)
}
options={optionsTransform(parameter.choices, "label")}
onFocus={() => setSelectedNodeId(batchId || "")}
onBlur={() => setSelectedNodeId("")}
error={parameter.error || false}
helperText={parameter.helperText}
></MyCheckBox>
)}
{parameter.description && (
<Tooltip title={parameter.description} placement="top">
<img
className={styles.parameterDesc}
src={questionMark}
alt=""
/>
</Tooltip>
)}
</div>
</div>
);
});
};
onBlur={()=> setSelectedNodeId('')}
value={parameter.value || ""}
onChange={(e: any) =>
handleParameterChange(e, taskId, parameter.name || "")
}
placeholder="请输入"
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "select" && (
<MySelect
onFocus={()=> setSelectedNodeId(batchId || '')}
onBlur={()=> setSelectedNodeId('')}
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(e, taskId, parameter.name || "")
}
error={parameter.error || false}
helpertext={parameter.helperText}
options={optionsTransform(parameter.choices, "label")}
></MySelect>
)}
{parameter.domType.toLowerCase() === "multipleselect" && (
<MySelect
onFocus={()=> setSelectedNodeId(batchId || '')}
onBlur={()=> setSelectedNodeId('')}
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(e, taskId, parameter.name || "")
}
multiple={true}
error={parameter.error || false}
helpertext={parameter.helperText}
options={optionsTransform(parameter.choices, "label")}
></MySelect>
)}
{parameter.domType.toLowerCase() === "radio" && (
<MyRadio
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(e, taskId, parameter.name || "")
}
onFocus={()=> setSelectedNodeId(batchId || '')}
onBlur={()=> setSelectedNodeId('')}
options={optionsTransform(parameter.choices, "label")}
error={parameter.error || false}
helperText={parameter.helperText}
></MyRadio>
)}
{parameter.domType.toLowerCase() === "checkbox" && (
<MyCheckBox
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(
{
target: {
value: e,
},
},
taskId,
parameter.name || ""
)
}
options={optionsTransform(parameter.choices, "label")}
onFocus={()=> setSelectedNodeId(batchId || '')}
onBlur={()=> setSelectedNodeId('')}
error={parameter.error || false}
helperText={parameter.helperText}
></MyCheckBox>
)}
{parameter.description && (
<Tooltip title={parameter.description} placement="top">
<img
className={styles.parameterDesc}
src={questionMark}
alt=""
/>
</Tooltip>
)}
</div>
</div>
);
});
};
return (
<div className={styles.formBox}>
<div className={styles.templateDescBox}>
<div className={styles.templateDescTitle}>模板描述</div>
<div className={styles.templateDesc}>
{templateConfigInfo?.description || ""}
</div>
</div>
<div
className={classnames({
[styles.backgroundTitle]: true,
[styles.backgroundTitlePass]: true,
})}
>
<img src="" alt="" />
<span className={styles.backgroundTitleText}>基础信息</span>
</div>
<div className={styles.formItems}>
<div className={styles.formItem}>
<div
className={classnames({
[styles.formItemLable]: true,
[styles.required]: true,
})}
>
任务名称
</div>
<div className={styles.formItem}>
<MyInput
value={name}
onChange={handleNameChange}
placeholder="请输入任务名称"
error={nameHelp.error}
helperText={nameHelp.helperText}
></MyInput>
</div>
</div>
<div className={styles.formItem}>
<div
className={classnames({
[styles.formItemLable]: true,
[styles.required]: true,
})}
>
输出路径
</div>
<div className={styles.formItem}>
<MyInput
value={outputPath || ""}
InputProps={{
endAdornment: (
<img
onClick={() => handleOpenFileSelect()}
src={fileSelectIcon}
alt="选择输出路径"
className={styles.fileSelectImg}
/>
),
}}
error={outputPathHelp.error}
helperText={outputPathHelp.helperText}
></MyInput>
</div>
</div>
</div>
{renderTasks.map((task, taskIndex) => {
return (
<div className={styles.taskBox} key={task.id + taskIndex}>
<div
className={classnames({
[styles.backgroundTitle]: true,
[styles.backgroundTitlePass]: true,
})}
>
<img
className={classnames({
[styles.backgroundTitleTextIcon]: true,
[styles.backgroundTitleTextIconShow]: task.isCheck,
})}
src={jobSueIcon}
alt=""
/>
<span id={`point${task.id}`} className={styles.backgroundTitleText}>{task.title}</span>
{task.description && (
<Tooltip title={task.description} placement="top">
<img className={styles.taskDescIcon} src={tipsIcon} alt="" />
</Tooltip>
)}
</div>
<div className={styles.taskConfigBox}>
{randerParameters(task.parameters, task.id, task.id)}
{task.flows.map((flow) => {
return (
<div className={styles.flowConfigBox} key={flow.id}>
<div className={styles.flowTitle}>
{flow.title}
{flow.description && (
<Tooltip title={flow.description} placement="top">
<img
className={styles.flowDescIcon}
src={tipsIcon}
alt=""
/>
</Tooltip>
)}
</div>
{randerParameters(flow.parameters, flow.id, flow.parentNode ? flow.parentNode : flow.id )}
</div>
);
})}
</div>
</div>
);
})}
{fileSelectOpen && (
<FileSelect
onClose={handleFileSelectOnClose}
open={fileSelectOpen}
onConfirm={onFileSelectConfirm}
/>
)}
</div>
);
return (
<div className={styles.formBox}>
<div className={styles.templateDescBox}>
<div className={styles.templateDescTitle}>模板描述</div>
<div className={styles.templateDesc}>
{templateConfigInfo?.description || ""}
</div>
</div>
<div
className={classnames({
[styles.backgroundTitle]: true,
[styles.backgroundTitlePass]: true,
})}
>
<img src="" alt="" />
<span className={styles.backgroundTitleText}>基础信息</span>
</div>
<div className={styles.formItems}>
<div className={styles.formItem}>
<div
className={classnames({
[styles.formItemLable]: true,
[styles.required]: true,
})}
>
任务名称
</div>
<div className={styles.formItem}>
<MyInput
value={name}
onChange={handleNameChange}
placeholder="请输入任务名称"
error={nameHelp.error}
helperText={nameHelp.helperText}
></MyInput>
</div>
</div>
<div className={styles.formItem}>
<div
className={classnames({
[styles.formItemLable]: true,
[styles.required]: true,
})}
>
输出路径
</div>
<div className={styles.formItem}>
<MyInput
value={outputPath || ""}
InputProps={{
endAdornment: (
<img
onClick={() => handleOpenFileSelect()}
src={fileSelectIcon}
alt="选择输出路径"
className={styles.fileSelectImg}
/>
),
}}
error={outputPathHelp.error}
helperText={outputPathHelp.helperText}
></MyInput>
</div>
</div>
</div>
{renderTasks.map((task, taskIndex) => {
return (
<div className={styles.taskBox} key={task.id + taskIndex}>
<div
className={classnames({
[styles.backgroundTitle]: true,
[styles.backgroundTitlePass]: true,
})}
>
<img
className={classnames({
[styles.backgroundTitleTextIcon]: true,
[styles.backgroundTitleTextIconShow]: task.isCheck,
})}
src={jobSueIcon}
alt=""
/>
<span
id={`point${task.id}`}
className={styles.backgroundTitleText}
>
{task.title}
</span>
{task.description && (
<Tooltip title={task.description} placement="top">
<img className={styles.taskDescIcon} src={tipsIcon} alt="" />
</Tooltip>
)}
</div>
<div className={styles.taskConfigBox}>
{randerParameters(task.parameters, task.id, task.id)}
{task.flows.map((flow) => {
return (
<div className={styles.flowConfigBox} key={flow.id}>
<div className={styles.flowTitle}>
{flow.title}
{flow.description && (
<Tooltip title={flow.description} placement="top">
<img
className={styles.flowDescIcon}
src={tipsIcon}
alt=""
/>
</Tooltip>
)}
</div>
{randerParameters(
flow.parameters,
flow.id,
flow.parentNode ? flow.parentNode : flow.id
)}
</div>
);
})}
</div>
</div>
);
})}
{fileSelectOpen && (
<FileSelect
onClose={handleFileSelectOnClose}
open={fileSelectOpen}
onConfirm={onFileSelectConfirm}
/>
)}
</div>
);
};
export default ConfigForm;
......@@ -2,7 +2,7 @@
* @Author: 吴永生#A02208 yongsheng.wu@wholion.com
* @Date: 2022-06-21 20:03:56
* @LastEditors: 吴永生#A02208 yongsheng.wu@wholion.com
* @LastEditTime: 2022-06-28 16:22:13
* @LastEditTime: 2022-07-07 17:39:49
* @FilePath: /bkunyun/src/views/Project/ProjectSubmitWork/interface.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
......@@ -40,7 +40,8 @@ export interface ITask {
x: number;
y: number;
};
type: IType;
tags?: string[];
type: IType | string;
parentNode?: string;
parameters: Array<IParameter>;
edges: Array<IEdge>;
......@@ -103,7 +104,7 @@ export type IRenderTask = {
x: number;
y: number;
};
type: IType;
type: IType | string;
parameters: Array<IParameter>;
edges: Array<IEdge>;
flows: ITask[];
......
......@@ -16,64 +16,64 @@ import WorkbenchTemplate from "./workbenchTemplate";
import WorkbenchList from "./workbenchList";
import Tabs from "@/components/mui/Tabs";
import usePass from "@/hooks/usePass";
import Template from "@/assets/project/workbenchTemplate.svg"
import Template_select from "@/assets/project/workbenchTemplate_select.svg"
import List from "@/assets/project/workbenchList.svg"
import List_select from "@/assets/project/workbenchList_select.svg"
import Template from "@/assets/project/workbenchTemplate.svg";
import Template_select from "@/assets/project/workbenchTemplate_select.svg";
import List from "@/assets/project/workbenchList.svg";
import List_select from "@/assets/project/workbenchList_select.svg";
const ProjectWorkbench = observer(() => {
const isPass = usePass();
const location: any = useLocation()
const tabList = useMemo(() => {
return [
{
label: "工作流模版",
value: "workbenchTemplate",
component: <WorkbenchTemplate />,
hide: !isPass("PROJECT_WORKBENCH_FLOES"),
icon: Template,
iconed: Template_select,
},
{
label: "任务列表",
value: "workbenchList",
component: <WorkbenchList />,
hide: !isPass("PROJECT_WORKBENCH_JOBS"),
icon: List,
iconed: List_select,
},
// {
// label: "按钮组件",
// value: "MUI_BUTTON",
// component: <ButtonDemo />,
// icon: List,
// iconed: List_select,
// },
// {
// label: "输入框组件",
// value: "MUI_INPUT",
// component: <InputDemo />,
// icon: List,
// iconed: List_select,
// },
];
}, [isPass]);
const isPass = usePass();
const location: any = useLocation();
const tabList = useMemo(() => {
return [
{
label: "工作流模版",
value: "workbenchTemplate",
component: <WorkbenchTemplate />,
hide: !isPass("PROJECT_WORKBENCH_FLOES"),
icon: Template,
iconed: Template_select,
},
{
label: "任务列表",
value: "workbenchList",
component: <WorkbenchList />,
hide: !isPass("PROJECT_WORKBENCH_JOBS"),
icon: List,
iconed: List_select,
},
// {
// label: "按钮组件",
// value: "MUI_BUTTON",
// component: <ButtonDemo />,
// icon: List,
// iconed: List_select,
// },
// {
// label: "输入框组件",
// value: "MUI_INPUT",
// component: <InputDemo />,
// icon: List,
// iconed: List_select,
// },
];
}, [isPass]);
return (
<div style={{ padding: 24 }}>
<div style={{ display: "flex", alignItems: "center" }}>
<img src={projectImg} alt="项目logo" />
<span style={{ marginLeft: 12 }}>
工作台
</span>
</div>
<Box sx={{ width: "100%", typography: "body1" }}>
<Tabs tabList={tabList} defaultValue={location?.state?.type || 'workbenchTemplate'} />
</Box>
</div>
);
return (
<div style={{ padding: 24 }}>
<div style={{ display: "flex", alignItems: "center" }}>
<img src={projectImg} alt="项目logo" />
<span style={{ marginLeft: 12 }}>工作台</span>
</div>
<Box sx={{ width: "100%", typography: "body1" }}>
<Tabs
tabList={tabList}
defaultValue={location?.state?.type || "workbenchTemplate"}
/>
</Box>
</div>
);
});
export default memo(ProjectWorkbench);
.addTemplateBox {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.78);
z-index: 100;
display: flex;
flex-direction: column;
}
.closeBox {
display: flex;
justify-content: flex-end;
height: 40px;
align-items: center;
/* background-color: ; */
}
.content {
flex: 1;
background-color: #fff;
border-radius: 16px 0 0 0;
padding: 24px 32px;
box-sizing: border-box;
overflow: scroll;
}
.templateList {
/* height: 2000px; */
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.templateLi {
height: 146px;
box-sizing: border-box;
padding: 16px 20px;
cursor: pointer;
border: 1px solid rgba(235, 237, 240, 1);
border-radius: 4px;
min-width: 20%;
flex: 1;
margin-right: 16px;
margin-bottom: 16px;
}
.templateLiCustom {
height: 194px;
}
.templateLiHidden {
visibility: hidden;
}
.addCustomTemplate {
height: 194px;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.addCustomTemplateText {
margin-top: 12px;
line-height: 22px;
font-size: 14px;
color: rgba(138, 144, 153, 1);
}
.templateLi:hover {
box-shadow: 6px 8px 22px 0px rgba(0, 24, 57, 0.08);
}
.templateLi:nth-child(4n) {
margin-right: 0;
}
.templateLiTop {
display: flex;
justify-content: space-between;
align-items: center;
}
.templateTitle {
font-size: 14px;
font-weight: 600;
color: #1e2633;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
line-height: 22px;
}
.templateLiInfo {
margin-bottom: 8px;
}
.templateLiInfoText {
line-height: 20px;
font-size: 12px;
color: rgba(19, 112, 255, 1);
}
.templateLiDesc {
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
display: -webkit-box;
height: 54px;
font-size: 12px;
color: rgba(138, 144, 153, 1);
}
.templateLiEditBox {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
import { memo, useEffect, useState, useMemo } from "react";
import style from "./index.module.css";
import classNames from "classnames";
import CloseOutlinedIcon from "@mui/icons-material/CloseOutlined";
import { Box, Typography } from "@mui/material";
import RadioGroupOfButtonStyle from "@/components/CommonComponents/RadioGroupOfButtonStyle";
import SearchIcon from "@mui/icons-material/Search";
import Button from "@/components/mui/Button";
import OutlinedInput from "@mui/material/OutlinedInput";
import Checkbox from "@mui/material/Checkbox";
import useMyRequest from "@/hooks/useMyRequest";
import AddIcon from "@mui/icons-material/Add";
import { useStores } from "@/store";
import WorkFlowEdit from "@/views/WorkFlowEdit";
import _ from "lodash";
import { observer } from "mobx-react-lite";
import noData from "../../../../../../assets/project/noTemplate.svg";
import { ICustomTemplate } from "../../interface";
import { getAddWorkbenchTemplate } from "@/api/workbench_api";
import { toJS } from "mobx";
type IAddTemplateProps = {
setShowAddTemplate: any;
};
const radioOptions = [
{
value: "public",
label: "公共",
},
{
value: "custom",
label: "自定义",
},
];
const AddTemplate = observer((props: IAddTemplateProps) => {
const { currentProjectStore } = useStores();
const projectId = toJS(currentProjectStore.currentProjectInfo.id);
const productId = toJS(currentProjectStore.currentProductInfo.id);
const { setShowAddTemplate } = props;
const handleSearch = (value: string) => {
console.log(value);
};
/** 可增加模板列表 */
const [addTemplateList, setAddTemplateList] = useState([]);
/** 已选择增加的模板列表 */
const [selectTemplateData, setSelectTemplateData] = useState<string[]>([]);
const [templateType, setTemplateType] = useState("public");
const handleRadio = (value: string) => {
setTemplateType(value);
};
const handleAddTemplate = () => {
console.log("handleAddTemplate");
};
// 添加工作流模板-获取模板列表
const { run: getAddTemplateList } = useMyRequest(getAddWorkbenchTemplate, {
onSuccess: (result: any) => {
console.log(result);
setAddTemplateList(result.data);
// setOpenAddTemplate(true);
},
});
/** 是否显示自定义模版编辑并带有参数 */
const [customTemplateInfo, setCustomTemplateInfo] = useState<ICustomTemplate>(
{
show: false,
}
);
// 显示新增、编辑自定义模板弹窗
const handleAddCustomTemplate = () => {
setCustomTemplateInfo({
show: true,
});
};
useEffect(() => {
getAddTemplateList({
projectId: projectId as string,
productId: productId as string,
});
}, [getAddTemplateList, projectId, productId]);
const hiddenBoxArr = useMemo(() => {
const length =
templateType === "public"
? addTemplateList.length
: addTemplateList.length + 1;
const hiddenBoxNumber = 4 - (length % 4);
const arr = new Array(hiddenBoxNumber).fill("");
return arr;
}, [addTemplateList, templateType]);
return (
<div className={style.addTemplateBox}>
<div className={style.closeBox}>
<CloseOutlinedIcon
sx={{ color: "#ffffff", marginRight: "10px", cursor: "pointer" }}
onClick={() => {
setShowAddTemplate(false);
}}
/>
</div>
<div className={style.content}>
<Typography
sx={{ fontSize: "18px", fontWeight: "600", color: "#1E2633" }}
>
添加工作流模版
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "20px",
}}
>
<OutlinedInput
onChange={(e: any) => {
_.debounce(() => {
// searchTemplateNameCallback(e.target.value);
handleSearch(e.target.value);
}, 200)();
}}
placeholder="输入关键词搜索"
size="small"
sx={{ width: 340, height: 32, marginTop: "20px" }}
endAdornment={<SearchIcon style={{ color: "#8A9099" }} />}
/>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
}}
>
<RadioGroupOfButtonStyle
value={templateType}
radioOptions={radioOptions}
handleRadio={handleRadio}
></RadioGroupOfButtonStyle>
<Button
// click={addTemplateCallback}
click={handleAddTemplate}
size={"small"}
style={{
marginLeft: "12px",
}}
text={
"添加模版" +
(selectTemplateData.length === 0
? ""
: `(${selectTemplateData.length})`)
}
/>
</Box>
</Box>
{templateType === "public" && addTemplateList.length === 0 && (
<Box
sx={{
display: "flex",
alignItems: "center",
flexDirection: "column",
minHeight: "calc(100vh - 376px)",
justifyContent: "center",
}}
>
<img alt="" src={noData} />
<Typography
sx={{ fontSize: "12px", fontWeight: "400", color: "#8A9099" }}
>
暂无相关模版
</Typography>
</Box>
)}
<div className={style.templateList}>
{templateType !== "public" && (
<div
className={classNames({
[style.templateLi]: true,
[style.addCustomTemplate]: true,
})}
onClick={handleAddCustomTemplate}
>
<AddIcon />
<span className={style.addCustomTemplateText}>
创建自定义模板
</span>
</div>
)}
{addTemplateList.map((item: any, index) => {
return (
<div
className={classNames({
[style.templateLi]: true,
[style.templateLiCustom]: templateType !== "public",
})}
key={index}
>
<div className={style.templateLiTop}>
<span className={style.templateTitle}>{item.title}</span>
<Checkbox
size="small"
sx={{ padding: "0px" }}
checked={selectTemplateData.includes(item.id)}
/>
</div>
<div className={style.templateLiInfo}>
<span
className={style.templateLiInfoText}
style={{ marginRight: "24px" }}
>
版本:{item.version}
</span>
<span className={style.templateLiInfoText}>
更新时间:{item.updateTime}
</span>
</div>
<div className={style.templateLiDesc}>{item.description}</div>
{templateType !== "public" && (
<div className={style.templateLiEditBox}>
<Button
click={handleAddTemplate}
size={"small"}
style={{
height: "32px",
}}
color="inherit"
text="编辑模板"
/>
</div>
)}
</div>
);
})}
{hiddenBoxArr.length !== 4 &&
hiddenBoxArr.map((item, index) => {
return (
<div
key={`-${index}`}
className={classNames({
[style.templateLi]: true,
[style.templateLiHidden]: true,
})}
/>
);
})}
</div>
</div>
{customTemplateInfo?.show ? (
<WorkFlowEdit
onBack={() =>
setCustomTemplateInfo({
id: "",
show: false,
})
}
/>
) : null}
</div>
);
});
export default AddTemplate;
......@@ -2,7 +2,7 @@
* @Author: 吴永生#A02208 yongsheng.wu@wholion.com
* @Date: 2022-05-31 10:18:13
* @LastEditors: 吴永生#A02208 yongsheng.wu@wholion.com
* @LastEditTime: 2022-07-06 21:25:00
* @LastEditTime: 2022-07-07 14:59:11
* @FilePath: /bkunyun/src/views/Project/ProjectSetting/index.tsx
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
......@@ -19,7 +19,8 @@ import Button from "@/components/mui/Button";
import useMyRequest from "@/hooks/useMyRequest";
import TemplateBox from "./components/templateBox";
import SimpleDialog from "./components/simpleDialog";
import AddTemplate from "./components/addTemplate";
// import AddTemplate from "./components/addTemplate";
import AddTemplate from "./components/AddTemplate/index";
import noData from "../../../../assets/project/noTemplate.svg";
import {
getWorkbenchTemplate,
......@@ -28,9 +29,7 @@ import {
addWorkbenchTemplate,
} from "@/api/workbench_api";
import usePass from "@/hooks/usePass";
import WorkFlowEdit from "@/views/WorkFlowEdit";
import { useStores } from "@/store";
import { ICustomTemplate } from "./interface";
import styles from "./index.module.css";
......@@ -54,13 +53,7 @@ const ProjectMembers = observer(() => {
const [addTemplateList, setAddTemplateList] = useState([]);
/** 已选择增加的模板列表 */
const [selectTemplateData, setSelectTemplateData] = useState<string[]>([]);
/** 是否显示自定义模版编辑并带有参数 */
const [customTemplateInfo, setCustomTemplateInfo] = useState<ICustomTemplate>(
{
show: false,
}
);
const [showAddTemplate, setShowAddTemplate] = useState(false);
// 获取模板列表
const { run: getTemplateInfo } = useMyRequest(getWorkbenchTemplate, {
......@@ -130,10 +123,11 @@ const ProjectMembers = observer(() => {
/** 增加模板 */
const addTemplateBlock = () => {
getAddTemplateList({
projectId: currentProjectStore.currentProjectInfo.id as string,
productId: "cadd",
});
setShowAddTemplate(true);
// getAddTemplateList({
// projectId: currentProjectStore.currentProjectInfo.id as string,
// productId: "cadd",
// });
};
/** 关闭增加模板 */
......@@ -231,7 +225,9 @@ const ProjectMembers = observer(() => {
{templateList &&
templateList.length > 0 &&
templateList.map((item, key) => {
return <TemplateBox data={item} startDialog={startDialog} />;
return (
<TemplateBox key={key} data={item} startDialog={startDialog} />
);
})}
</Box>
)}
......@@ -260,7 +256,7 @@ const ProjectMembers = observer(() => {
</Box>
)}
<AddTemplate
{/* <AddTemplate
openAddTemplate={openAddTemplate}
closeAddTemplateBlock={closeAddTemplateBlock}
addTemplateList={addTemplateList}
......@@ -268,9 +264,11 @@ const ProjectMembers = observer(() => {
selectTemplateData={selectTemplateData}
addTemplateCallback={addTemplateCallback}
searchTemplateNameCallback={searchTemplateNameCallback}
/>
/> */}
{customTemplateInfo?.show ? <WorkFlowEdit /> : null}
{showAddTemplate && (
<AddTemplate setShowAddTemplate={setShowAddTemplate} />
)}
<SimpleDialog
text={"确认移除该模板吗?"}
......
import { OutlinedInput } from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import classNames from "classnames";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { toJS } from "mobx";
import cloneDeep from "lodash/cloneDeep";
import { mockData } from "./mock";
import { IOperatorItemProps } from "./interface";
import { IOperatorItemProps, IOperatorListProps } from "./interface";
import { ITask } from "@/views/Project/ProjectSubmitWork/interface";
import useMyRequest from "@/hooks/useMyRequest";
import { IResponse } from "@/api/http";
import { fetchOperatorList } from "@/api/workbench_api";
import { useStores } from "@/store";
import { uuid } from "@/utils/util";
import styles from "./index.module.css";
......@@ -12,12 +21,16 @@ import styles from "./index.module.css";
* @Author: 吴永生#A02208 yongsheng.wu@wholion.com
* @Date: 2022-07-06 15:16:01
* @LastEditors: 吴永生#A02208 yongsheng.wu@wholion.com
* @LastEditTime: 2022-07-06 21:23:19
* @LastEditTime: 2022-07-07 15:48:12
* @FilePath: /bkunyun/src/views/WorkFlowEdit/components/OperatorList/index.tsx
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
const OperatorItem = (props: IOperatorItemProps) => {
const { info } = props;
const {
info: { title, description, tags },
setTemplateConfigInfo,
templateConfigInfo,
} = props;
const [isDragStyle, setIsDragStyle] = useState<boolean>(false);
/** 拖拽开始 */
......@@ -26,10 +39,29 @@ const OperatorItem = (props: IOperatorItemProps) => {
}, []);
/** 拖拽结束 */
const onDragEnd = useCallback((e: React.DragEvent<HTMLDivElement>) => {
console.log(e);
setIsDragStyle(false);
}, []);
const onDragEnd = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
const dom = document.getElementById("workFlowEditRight");
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 (
e.clientX > upperLeftPointX &&
e.clientY > upperLeftPointY &&
e.clientX < lowerRightX &&
e.clientY < lowerRightY
) {
const newVal = [
...cloneDeep(templateConfigInfo),
{ ...props.info, uuid: uuid() },
];
setTemplateConfigInfo(newVal);
}
setIsDragStyle(false);
},
[setTemplateConfigInfo, templateConfigInfo]
);
return (
<div
......@@ -41,27 +73,55 @@ const OperatorItem = (props: IOperatorItemProps) => {
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
<h2 className={styles.operatorItemTitle}>说什么呢啊</h2>
<div className={styles.operatorItemText}>
STU utility
是一个R-packa标处理目标处理,目标处理目标处理标处理目标处理后期委屈好委屈农,博啊发布丢我被欺安度切换阿斯顿几切换,i的亲戚我好奇你eqeqeweqeqeeqeqeqeqeq。
</div>
<h2 className={styles.operatorItemTitle}>{title}</h2>
<div className={styles.operatorItemText}>{description}</div>
<div className={styles.footerBox}>
<span
className={styles.labelBox}
style={{
background: true ? "#EBF3FF" : "#E3FAEC",
color: true ? "#1370FF" : "#02AB83",
}}
>
公共平台
</span>
{tags?.map((item: string) => {
return (
<span
key={item}
className={styles.labelBox}
style={{
background: true ? "#EBF3FF" : "#E3FAEC",
color: true ? "#1370FF" : "#02AB83",
}}
>
{item}
</span>
);
})}
{/* <MySelect options={[]} /> */}
</div>
</div>
);
};
const OperatorList = () => {
const OperatorList = observer((props: IOperatorListProps) => {
const { currentProjectStore } = useStores();
const productId = toJS(currentProjectStore.currentProductInfo.id);
const { templateConfigInfo, setTemplateConfigInfo } = props;
const [operatorListData, setOperatorListData] = useState<ITask[]>(
mockData as any
);
console.log(templateConfigInfo, "templateConfigInfo");
// 取消作业
const { run } = useMyRequest(fetchOperatorList, {
onSuccess: (res: IResponse<any>) => {
console.log(res, "1111");
},
});
useEffect(() => {
run({
owner: "root",
productId: "cadd" || "",
// keyword : ''
});
}, [productId, run]);
return (
<div className={styles.operatorListBox}>
<div className={styles.searchBox}>
......@@ -69,7 +129,6 @@ const OperatorList = () => {
onChange={(e: any) => {
console.log(e.target.value);
}}
// value={templateName}
placeholder="输入关键词搜索"
size="small"
sx={{ height: 32, width: "100%" }}
......@@ -77,12 +136,21 @@ const OperatorList = () => {
/>
</div>
<div className={styles.listBox}>
{mockData.map((item) => {
return <OperatorItem key={item.id} info={item} />;
})}
{operatorListData
.filter((item) => item.type === "BATCH")
.map((item) => {
return (
<OperatorItem
key={item.id}
info={item}
templateConfigInfo={templateConfigInfo}
setTemplateConfigInfo={setTemplateConfigInfo}
/>
);
})}
</div>
</div>
);
};
});
export default OperatorList;
import { ITask } from "@/views/Project/ProjectSubmitWork/interface"
/*
* @Author: 吴永生#A02208 yongsheng.wu@wholion.com
* @Date: 2022-07-06 15:32:11
* @LastEditors: 吴永生#A02208 yongsheng.wu@wholion.com
* @LastEditTime: 2022-07-06 15:32:42
* @LastEditTime: 2022-07-07 16:22:08
* @FilePath: /bkunyun/src/views/WorkFlowEdit/components/OperatorList/interface.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
export interface IOperatorItemProps {
info: any
info: ITask
setTemplateConfigInfo: (val: ITask[]) => void;
templateConfigInfo: ITask[]
}
export interface IOperatorListProps {
templateConfigInfo: ITask[]
setTemplateConfigInfo: (val: ITask[]) => void
}
\ No newline at end of file
......@@ -40,3 +40,7 @@
flex: 1;
height: calc(100vh - 56px);
}
.radiosBox {
background-color: #fff;
padding: 24px;
}
......@@ -2,27 +2,44 @@
* @Author: 吴永生#A02208 yongsheng.wu@wholion.com
* @Date: 2022-06-21 20:03:56
* @LastEditors: 吴永生#A02208 yongsheng.wu@wholion.com
* @LastEditTime: 2022-07-06 18:35:24
* @LastEditTime: 2022-07-08 09:25:42
* @FilePath: /bkunyun/src/views/Project/ProjectSubmitWork/index.tsx
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import React, { useState } from "react";
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
import IconButton from "@mui/material/IconButton";
import { useLocation, useNavigate } from "react-router-dom";
import MyPopconfirm from "@/components/mui/MyPopconfirm";
import RadioGroupOfButtonStyle from "@/components/CommonComponents/RadioGroupOfButtonStyle";
import ButtonComponent from "@/components/mui/Button";
import { ITemplateConfig } from "../Project/ProjectSubmitWork/interface";
import OperatorList from "./components/OperatorList";
import Flow from "../Project/components/Flow";
import { ITask } from "../Project/ProjectSubmitWork/interface";
import styles from "./index.module.css";
import { style } from "@mui/system";
const WorkFlowEdit = () => {
const [templateConfigInfo, setTemplateConfigInfo] =
useState<ITemplateConfig>();
const location: any = useLocation();
const navigate = useNavigate();
const radioOptions = [
{
value: "list",
label: "算子列表",
},
{
value: "setting",
label: "参数设置",
},
];
interface IProps {
onBack?: () => void;
}
const WorkFlowEdit = (props: IProps) => {
const { onBack } = props;
const [templateConfigInfo, setTemplateConfigInfo] = useState<ITask[]>([]);
const [leftContentType, setLeftContentType] = useState("list");
return (
<div className={styles.swBox}>
......@@ -30,11 +47,10 @@ const WorkFlowEdit = () => {
<div className={styles.swHeaderLeft}>
<MyPopconfirm
title="返回后,当前页面已填写内容将不保存,确认返回吗?"
onConfirm={() => console.log(11)}
onConfirm={onBack}
>
<IconButton
color="primary"
// onClick={() => handleGoBack()}
aria-label="upload picture"
component="span"
size="small"
......@@ -62,10 +78,35 @@ const WorkFlowEdit = () => {
</div>
</div>
<div className={styles.swContent}>
<div className={styles.swFormBox}>
<OperatorList />
<div>
<div className={styles.radiosBox}>
<RadioGroupOfButtonStyle
radioOptions={radioOptions}
value={leftContentType}
handleRadio={setLeftContentType}
RadiosBoxStyle={{
height: "36px",
padding: "3px",
}}
radioStyle={{
fontSize: "16px",
height: "30px",
}}
></RadioGroupOfButtonStyle>
</div>
{leftContentType === "list" && (
<div className={styles.swFormBox}>
<OperatorList
templateConfigInfo={templateConfigInfo}
setTemplateConfigInfo={setTemplateConfigInfo}
/>
</div>
)}
{leftContentType !== "list" && <div>123</div>}
</div>
<div id="workFlowEditRight">
<Flow tasks={templateConfigInfo} />
</div>
<div className={styles.swFlowBox}>右侧</div>
</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