Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
B
bkunyun
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
sunyihao
bkunyun
Commits
2a7b6d6a
Commit
2a7b6d6a
authored
Jun 27, 2022
by
chenshouchao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 提交任务接口联调
parent
fa9375be
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
322 additions
and
190 deletions
+322
-190
package-lock.json
package-lock.json
+0
-0
api_manager.ts
src/api/api_manager.ts
+1
-0
project_api.ts
src/api/project_api.ts
+19
-4
MyInput.tsx
src/components/mui/MyInput.tsx
+1
-1
index.module.css
...ews/Project/ProjectSubmitWork/ConfigForm/index.module.css
+14
-3
index.tsx
src/views/Project/ProjectSubmitWork/ConfigForm/index.tsx
+259
-177
index.tsx
src/views/Project/ProjectSubmitWork/index.tsx
+23
-1
interface.ts
src/views/Project/ProjectSubmitWork/interface.ts
+5
-4
No files found.
package-lock.json
View file @
2a7b6d6a
This source diff could not be displayed because it is too large. You can
view the blob
instead.
src/api/api_manager.ts
View file @
2a7b6d6a
...
...
@@ -34,6 +34,7 @@ const RESTAPI = {
API_WORKBENCH_WORKFLOWJOB_LIST
:
`
${
BACKEND_API_URI_PREFIX
}
/cpp/workbench/project/workflowjob`
,
//查询工作流任务
API_WORKBENCH_DEL_WORKFLOWJOB
:
`
${
BACKEND_API_URI_PREFIX
}
/cpp/workflow/job/`
,
//删除工作流任务
API_WORKBENCH_CANCEL_WORKFLOWJOB
:
`
${
BACKEND_API_URI_PREFIX
}
/cpp/workflow/cancel`
,
//取消工作流
API_SUBMIT_WORKFLOW
:
`
${
BACKEND_API_URI_PREFIX
}
/cpp/workflow/submit`
,
//提交工作流
};
export
default
RESTAPI
;
src/api/project_api.ts
View file @
2a7b6d6a
...
...
@@ -202,9 +202,8 @@ const getDataFileDelPackage = (params: getDataFileDelPackageParams) => {
});
};
// 点击使用模版,获取模版数据
const
fetchTemplateConfigInfo
=
(
params
:
{
id
:
string
})
=>
{
const
fetchTemplateConfigInfo
=
(
params
:
{
id
:
string
})
=>
{
return
request
({
url
:
`
${
Api
.
API_FETCH_TEMPLATE_INFO
}
/
${
params
.
id
}
`
,
method
:
"get"
,
...
...
@@ -212,14 +211,29 @@ const fetchTemplateConfigInfo = (params: {id: string}) => {
};
// 点击工作列表,查看工作流详情
const
fetchWorkFlowJob
=
(
params
:
{
id
:
string
})
=>
{
const
fetchWorkFlowJob
=
(
params
:
{
id
:
string
})
=>
{
return
request
({
url
:
`
${
Api
.
API_WORK_FLOW_JOB
}
/
${
params
.
id
}
`
,
method
:
"get"
,
});
};
type
submitWorkFlowParams
=
{
name
:
string
;
projectId
:
string
;
specId
:
string
;
outputPath
:
string
;
promotedParameters
:
any
;
};
// 提交工作流
const
submitWorkFlow
=
(
params
:
submitWorkFlowParams
)
=>
{
return
request
({
url
:
Api
.
API_SUBMIT_WORKFLOW
,
method
:
"post"
,
data
:
params
,
});
};
export
{
current
,
...
...
@@ -238,5 +252,6 @@ export {
getDataFileDel
,
getDataFileDelPackage
,
fetchTemplateConfigInfo
,
fetchWorkFlowJob
fetchWorkFlowJob
,
submitWorkFlow
,
};
src/components/mui/MyInput.tsx
View file @
2a7b6d6a
...
...
@@ -12,7 +12,7 @@ type MyInputProps = {
placeholder
?:
string
;
fullWidth
?:
boolean
;
// 宽度是否和容器一致
InputProps
?:
any
;
// input加前后icon可以用这个
error
?:
boolean
;
error
?:
boolean
;
helperText
?:
string
;
};
...
...
src/views/Project/ProjectSubmitWork/ConfigForm/index.module.css
View file @
2a7b6d6a
...
...
@@ -26,10 +26,10 @@
box-sizing
:
border-box
;
position
:
relative
;
}
.backgroundTitleTextIcon
{
.backgroundTitleTextIcon
{
visibility
:
hidden
;
}
.backgroundTitleTextIconShow
{
.backgroundTitleTextIconShow
{
visibility
:
visible
;
}
.backgroundTitleText
{
...
...
@@ -65,6 +65,17 @@
.taskConfigBox
{
padding
:
24px
44px
40px
44px
;
}
.flowTitle
{
/* line-height: 22px; */
line-height
:
16px
;
/* margin-bottom: 24px; */
margin
:
3px
0
27px
;
color
:
rgba
(
30
,
38
,
51
,
1
);
font-size
:
14px
;
font-weight
:
600
;
border-left
:
3px
solid
rgba
(
19
,
112
,
255
,
1
);
padding-left
:
3px
;
}
.parameter
{
margin-bottom
:
20px
;
position
:
relative
;
...
...
@@ -76,7 +87,7 @@
line-height
:
22px
;
margin-bottom
:
12px
;
}
.parameterContent
{
.parameterContent
{
position
:
relative
;
}
.parameterDesc
{
...
...
src/views/Project/ProjectSubmitWork/ConfigForm/index.tsx
View file @
2a7b6d6a
...
...
@@ -14,11 +14,12 @@ import { getCheckResult } from "../util";
import
fileSelectIcon
from
"@/assets/project/fileSelect.svg"
;
import
questionMark
from
"@/assets/project/questionMark.svg"
;
import
jobSueIcon
from
"@/assets/project/jobSue.svg"
;
import
{
IParameter
}
from
"../interface"
;
type
ConfigFormProps
=
{
templateConfigInfo
?:
ITemplateConfig
;
setParameter
:
any
;
onRef
?:
React
.
Ref
<
any
>
onRef
?:
React
.
Ref
<
any
>
;
};
const
ConfigForm
=
(
props
:
ConfigFormProps
)
=>
{
...
...
@@ -30,20 +31,20 @@ const ConfigForm = (props: ConfigFormProps) => {
); // 任务名称
const [nameHelp, setNameHelp] = useState({
error: false,
helperText:
''
})
helperText:
"",
})
;
const [outputPath, setOutputPath] = useState<string>("ProjectData"); // 输出路径
const [outputPathHelp, setOutputPathHelp] = useState({
error: false,
helperText:
''
})
helperText:
"",
})
;
const getNameAndPath = () => {
return {
name,
outputPath,
nameAndOutputPathCheck: !(checkName(name) || checkOutputPath(outputPath)) // 任务名称、输出路径是否通过校验
}
}
nameAndOutputPathCheck: !(checkName(name) || checkOutputPath(outputPath))
,
// 任务名称、输出路径是否通过校验
}
;
}
;
useImperativeHandle(props.onRef, () => {
return {
...
...
@@ -51,60 +52,80 @@ const ConfigForm = (props: ConfigFormProps) => {
};
});
const [fileSelectOpen, setFileSelectOpen] = useState(false); // 选择输出路径的弹窗显示控制
const [fileSelectObject, setFileSelectObject] = useState({
taskId: "",
parameterName: "",
});
const onFileSelectConfirm = (path: string) => {
setFileSelectOpen(false);
setOutputPath(`
ProjectData$
{
path
===
"/"
?
""
:
path
}
`);
checkOutputPath(`
ProjectData$
{
path
===
"/"
?
""
:
path
}
`)
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 handleOpenFileSelect = () => {
const handleOpenFileSelect = (
taskId: string = "",
parameterName: string = ""
) => {
setFileSelectObject({
taskId,
parameterName,
});
setFileSelectOpen(true);
};
const handleNameChange = (e: any) => {
setName(e.target.value);
checkName(e.target.value)
checkName(e.target.value)
;
};
const checkName = (name: string =
'')=>
{
const reg = new RegExp(/^[a-zA-Z0-9\u4e00-\u9fa5-_]{3,30}$/)
const checkName = (name: string =
"") =>
{
const reg = new RegExp(/^[a-zA-Z0-9\u4e00-\u9fa5-_]{3,30}$/)
;
if (reg.test(name)) {
setNameHelp({
error: false,
helperText:
''
})
return false
helperText:
"",
})
;
return false
;
} else {
setNameHelp({
error: true,
helperText: '请输入正确任务名称(3~30字符,可包含大小写字母、数字、中文、特殊符号“-”、“_”)'
})
return true
helperText:
"请输入正确任务名称(3~30字符,可包含大小写字母、数字、中文、特殊符号“-”、“_”)",
});
return true;
}
}
}
;
const checkOutputPath = (outputPath: string =
'')=>
{
const checkOutputPath = (outputPath: string =
"") =>
{
if (outputPath) {
setOutputPathHelp({
error: false,
helperText:
''
})
return false
helperText:
"",
})
;
return false
;
} else {
setOutputPathHelp({
error: true,
helperText:
'请选择输出路径'
})
return true
helperText:
"请选择输出路径",
})
;
return true
;
}
}
}
;
const renderTasks: IRenderTasks = useMemo(() => {
const result: IRenderTasks = [];
...
...
@@ -116,31 +137,31 @@ const ConfigForm = (props: ConfigFormProps) => {
}
});
result.forEach((task) => {
let isCheck = true
let isCheck = true
;
if (task.parameters.length > 0) {
task.parameters.forEach((parameter)
=>
{
const { error } = getCheckResult(parameter, parameter.value)
task.parameters.forEach((parameter)
=>
{
const { error } = getCheckResult(parameter, parameter.value)
;
if (error) {
isCheck = false
return
isCheck = false
;
return
;
}
})
})
;
}
if
(task.flows.length>
0) {
if
(task.flows.length >
0) {
task.flows.forEach((flow) => {
if (flow.parameters.length > 0) {
flow.parameters.forEach((parameter)
=>
{
const { error } = getCheckResult(parameter, parameter.value)
flow.parameters.forEach((parameter)
=>
{
const { error } = getCheckResult(parameter, parameter.value)
;
if (error) {
isCheck = false
return
isCheck = false
;
return
;
}
})
})
;
}
})
})
;
}
task.isCheck = isCheck
})
task.isCheck = isCheck
;
})
;
return result;
}, [templateConfigInfo]);
...
...
@@ -152,6 +173,182 @@ const ConfigForm = (props: ConfigFormProps) => {
setParameter(e.target.value, taskId, parameterName);
};
const randerParameters = (parameters: Array<IParameter>, taskId: 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.dataType}
</span>
</div>
<div className={styles.parameterContent}>
{parameter.domType.toLowerCase() === "file" && (
<MyInput
value={parameter.value || ""}
InputProps={{
endAdornment: (
<img
onClick={() =>
handleOpenFileSelect(taskId, parameter.name)
}
src={fileSelectIcon}
alt=""
className={styles.fileSelectImg}
/>
),
}}
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "pathselect" && (
<MyInput
value={parameter.value || ""}
InputProps={{
endAdornment: (
<img
onClick={() =>
handleOpenFileSelect(taskId, parameter.name)
}
src={fileSelectIcon}
alt=""
className={styles.fileSelectImg}
/>
),
}}
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "datasetselect" && (
<MyInput
value={parameter.value || ""}
InputProps={{
endAdornment: (
<img
onClick={() =>
handleOpenFileSelect(taskId, parameter.name)
}
src={fileSelectIcon}
alt=""
className={styles.fileSelectImg}
/>
),
}}
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "fileselect" && (
<MyInput
value={parameter.value || ""}
InputProps={{
endAdornment: (
<img
onClick={() =>
handleOpenFileSelect(taskId, parameter.name)
}
src={fileSelectIcon}
alt=""
className={styles.fileSelectImg}
/>
),
}}
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "input" && (
<MyInput
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
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(e, taskId, parameter.name || "")
}
error={parameter.error || false}
helperText={parameter.helperText}
options={optionsTransform(parameter.choices, "key")}
></MySelect>
)}
{parameter.domType.toLowerCase() === "multipleselect" && (
<MySelect
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(e, taskId, parameter.name || "")
}
multiple={true}
error={parameter.error || false}
helperText={parameter.helperText}
options={optionsTransform(parameter.choices, "key")}
></MySelect>
)}
{parameter.domType.toLowerCase() === "radio" && (
<MyRadio
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(e, taskId, parameter.name || "")
}
options={optionsTransform(parameter.choices, "key")}
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, "key")}
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}>
...
...
@@ -200,11 +397,11 @@ const ConfigForm = (props: ConfigFormProps) => {
</div>
<div className={styles.formItem}>
<MyInput
value={outputPath}
value={outputPath
|| ""
}
InputProps={{
endAdornment: (
<img
onClick={
handleOpenFileSelect
}
onClick={
() => handleOpenFileSelect()
}
src={fileSelectIcon}
alt="选择输出路径"
className={styles.fileSelectImg}
...
...
@@ -226,122 +423,23 @@ const ConfigForm = (props: ConfigFormProps) => {
[styles.backgroundTitlePass]: true,
})}
>
<img
className={classnames({
[styles.backgroundTitleTextIcon]: true,
[styles.backgroundTitleTextIconShow]: task.isCheck,
})}
src={jobSueIcon} alt="" />
<img
className={classnames({
[styles.backgroundTitleTextIcon]: true,
[styles.backgroundTitleTextIconShow]: task.isCheck,
})}
src={jobSueIcon}
alt=""
/>
<span className={styles.backgroundTitleText}>{task.title}</span>
</div>
<div className={styles.taskConfigBox}>
{task.parameters.filter(parameter => parameter.hidden === false).map((parameter, parameterIndex) => {
{randerParameters(task.parameters, task.id)}
{task.flows.map((flow) => {
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.dataType}
</span>
</div>
<div className={styles.parameterContent}>
{parameter.domType.toLowerCase() === "input" && (
<MyInput
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(
e,
task.id,
parameter.name || ""
)
}
placeholder="请输入"
error={parameter.error || false}
helperText={parameter.helperText}
></MyInput>
)}
{parameter.domType.toLowerCase() === "select" && (
<MySelect
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(
e,
task.id,
parameter.name || ""
)
}
error={parameter.error || false}
helperText={parameter.helperText}
options={optionsTransform(parameter.choices, "key")}
></MySelect>
)}
{parameter.domType.toLowerCase() === "multipleselect" && (
<MySelect
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(
e,
task.id,
parameter.name || ""
)
}
multiple={true}
error={parameter.error || false}
helperText={parameter.helperText}
options={optionsTransform(parameter.choices, "key")}
></MySelect>
)}
{parameter.domType.toLowerCase() === "radio" && (
<MyRadio
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(
e,
task.id,
parameter.name || ""
)
}
options={optionsTransform(parameter.choices, "key")}
error={parameter.error || false}
helperText={parameter.helperText}
></MyRadio>
)}
{parameter.domType.toLowerCase() === "checkbox" && (
<MyCheckBox
value={parameter.value}
onChange={(e: any) =>
handleParameterChange(
{
target: {
value: e
}
},
task.id,
parameter.name || ""
)
}
options={optionsTransform(parameter.choices, "key")}
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>
{/* question mark */}
<div className={styles.flowConfigBox} key={flow.id}>
<div className={styles.flowTitle}>{flow.title}</div>
{randerParameters(flow.parameters, flow.id)}
</div>
);
})}
...
...
@@ -349,27 +447,11 @@ const ConfigForm = (props: ConfigFormProps) => {
</div>
);
})}
{/* <MySelect
value={selectValue}
onChange={handleSelectChange}
multiple={true}
options={optionsTransform(options, "key")}
></MySelect> */}
{/* <MyCheckBox
value={selectValue}
onChange={handleCheckBoxChange}
options={optionsTransform(options, "key")}
></MyCheckBox> */}
{/* <MyRadio
value={selectValue}
onChange={handleRadioChange}
options={optionsTransform(options, "key")}
></MyRadio> */}
{/* <FileSelect
<FileSelect
onClose={handleFileSelectOnClose}
open={fileSelectOpen}
onConfirm={onFileSelectConfirm}
/>
*/}
/>
</div>
);
};
...
...
src/views/Project/ProjectSubmitWork/index.tsx
View file @
2a7b6d6a
...
...
@@ -16,15 +16,19 @@ import IconButton from "@mui/material/IconButton";
import
{
ITemplateConfig
}
from
"./interface"
;
import
_
from
"lodash"
;
import
useMyRequest
from
"@/hooks/useMyRequest"
;
import
{
fetchTemplateConfigInfo
}
from
"@/api/project_api"
;
import
{
fetchTemplateConfigInfo
,
submitWorkFlow
}
from
"@/api/project_api"
;
import
{
useLocation
,
useNavigate
}
from
"react-router-dom"
;
import
{
getCheckResult
}
from
"./util"
;
import
{
IResponse
}
from
"@/api/http"
;
import
{
templateConfigJson
}
from
"./mock"
;
import
{
useMessage
}
from
"@/components/MySnackbar"
;
import
{
toJS
}
from
"mobx"
;
import
{
useStores
}
from
"@/store"
;
const
ProjectSubmitWork
=
()
=>
{
const
Message
=
useMessage
();
const
{
currentProjectStore
}
=
useStores
();
const
projectId
=
toJS
(
currentProjectStore
.
currentProjectInfo
.
id
);
const
[
templateConfigInfo
,
setTemplateConfigInfo
]
=
useState
<
ITemplateConfig
>
();
const
location
:
any
=
useLocation
();
...
...
@@ -42,6 +46,12 @@ const ProjectSubmitWork = () => {
// }
});
const
{
run
:
submitWorkFlowRun
}
=
useMyRequest
(
submitWorkFlow
,
{
onSuccess
:
(
res
)
=>
{
console
.
log
(
res
);
},
});
useEffect
(()
=>
{
run
({
id
:
location
?.
state
?.
id
,
...
...
@@ -52,6 +62,7 @@ const ProjectSubmitWork = () => {
const
result
:
ITemplateConfig
=
_
.
cloneDeep
(
templateConfigInfo
);
result
.
tasks
.
forEach
((
tack
)
=>
{
if
(
tack
.
id
===
taskId
)
{
let
isCheck
=
true
;
tack
.
parameters
.
forEach
((
parameter
)
=>
{
if
(
parameter
.
name
===
parameterName
)
{
parameter
.
value
=
value
;
...
...
@@ -61,6 +72,10 @@ const ProjectSubmitWork = () => {
}
else
{
return
;
}
if
(
getCheckResult
(
parameter
,
value
).
error
===
true
)
{
isCheck
=
false
;
}
tack
.
isCheck
=
isCheck
;
});
}
else
{
return
;
...
...
@@ -90,6 +105,13 @@ const ProjectSubmitWork = () => {
setTemplateConfigInfo
(
result
);
if
(
check
)
{
console
.
log
(
"提交任务"
);
submitWorkFlowRun
({
name
,
outputPath
,
projectId
:
projectId
as
string
,
specId
:
templateConfigInfo
?.
id
as
string
,
promotedParameters
:
{},
});
}
else
{
Message
.
error
(
"请完善左侧表单再提交"
);
}
...
...
src/views/Project/ProjectSubmitWork/interface.ts
View file @
2a7b6d6a
...
...
@@ -6,7 +6,7 @@
* @FilePath: /bkunyun/src/views/Project/ProjectSubmitWork/interface.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
type
IType
=
'BATCH'
|
'FLOW'
type
IType
=
"BATCH"
|
"FLOW"
;
export
interface
IParameter
{
hidden
:
boolean
;
id
?:
string
;
...
...
@@ -24,8 +24,8 @@ export interface IParameter {
tasks
:
ITask
[];
validators
:
Array
<
IValidator
>
;
choices
:
Array
<
IChoice
>
;
error
?
:
boolean
;
helperText
?
:
string
;
error
?:
boolean
;
helperText
?:
string
;
}
export
interface
ITask
{
...
...
@@ -40,9 +40,9 @@ export interface ITask {
parentNode
?:
string
;
parameters
:
Array
<
IParameter
>
;
edges
:
Array
<
IEdge
>
;
isCheck
?:
boolean
;
}
export
interface
ITemplateConfig
{
title
:
string
;
version
:
string
;
...
...
@@ -54,6 +54,7 @@ export interface ITemplateConfig {
source
:
string
;
productId
:
string
;
tasks
:
ITask
[];
id
:
string
;
}
export
type
IDomType
=
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment