feat(devops): 实现流水线节点执行功能
- 新增 PipelineJobBean 类作为流水线任务载体 - 重构 PipelineJobManage 类,支持节点级任务管理 - 新增 PipelineNodeJobManage 类用于管理节点任务 - 实现节点任务的启动、中断和删除功能 - 新增节点日志切面和切面类
This commit is contained in:
@@ -4,13 +4,13 @@
|
||||
:style="{
|
||||
borderTopWidth: '5px',
|
||||
borderTopStyle: 'solid',
|
||||
borderTopColor: statusColorConst[templateData.status].color
|
||||
borderTopColor: statusColorConst[pipelineData.currentNodeStatus ? pipelineData.currentNodeStatus : statusColorConst.pending.code].color
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<div style="float: left;">
|
||||
<i
|
||||
v-if="templateData.currentNodeStatus === 'pending'"
|
||||
v-if="pipelineData.currentNodeStatus && pipelineData.currentNodeStatus === statusColorConst.pending.code"
|
||||
class="el-icon-time"
|
||||
:style="{
|
||||
fontSize: '22px',
|
||||
@@ -18,7 +18,7 @@
|
||||
}"
|
||||
/>
|
||||
<i
|
||||
v-else-if="templateData.currentNodeStatus === 'running'"
|
||||
v-else-if="pipelineData.currentNodeStatus && pipelineData.currentNodeStatus === statusColorConst.running.code"
|
||||
class="el-icon-loading"
|
||||
:style="{
|
||||
fontSize: '22px',
|
||||
@@ -26,7 +26,7 @@
|
||||
}"
|
||||
/>
|
||||
<i
|
||||
v-else-if="templateData.currentNodeStatus === 'success'"
|
||||
v-else-if="pipelineData.currentNodeStatus && pipelineData.currentNodeStatus === statusColorConst.success.code"
|
||||
class="el-icon-success"
|
||||
:style="{
|
||||
fontSize: '22px',
|
||||
@@ -34,7 +34,7 @@
|
||||
}"
|
||||
/>
|
||||
<i
|
||||
v-else-if="templateData.currentNodeStatus === 'fail'"
|
||||
v-else-if="pipelineData.currentNodeStatus && pipelineData.currentNodeStatus === statusColorConst.fail.code"
|
||||
class="el-icon-error"
|
||||
:style="{
|
||||
fontSize: '22px',
|
||||
@@ -43,25 +43,25 @@
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="el-icon-error"
|
||||
class="el-icon-time"
|
||||
:style="{
|
||||
fontSize: '22px',
|
||||
color: statusColorConst.fail.color
|
||||
color: statusColorConst.pending.color
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-name">{{ templateData.name }}</div>
|
||||
<div class="box-name">{{ pipelineData.name }}</div>
|
||||
<div style="clear: both" />
|
||||
</div>
|
||||
<div class="box-current-node">
|
||||
<a @click="onCurrentNodeClick">
|
||||
{{ templateData.currentNodeMsg }}
|
||||
{{ pipelineData.currentNodeMsg ? pipelineData.currentNodeMsg : statusColorConst.pending.label }}
|
||||
</a>
|
||||
</div>
|
||||
<el-row>
|
||||
<el-col :span="12" style="text-align: left">
|
||||
<!-- 流水线状态为fail,且节点支持重试 -->
|
||||
<div v-if="templateData.status === 'fail' && templateData.retry === true">
|
||||
<div v-if="pipelineData.currentNodeStatus === 'fail' && pipelineData.retry === true">
|
||||
<div class="box-buttons">
|
||||
<el-button
|
||||
type="text"
|
||||
@@ -77,30 +77,13 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 流水线状态为running,且有操作按钮 -->
|
||||
<div v-else-if="templateData.status === 'running' && templateData.buttons && templateData.buttons.length > 0">
|
||||
<div class="box-buttons">
|
||||
<el-button
|
||||
v-for="buttonItem in templateData.buttons"
|
||||
:key="buttonItem.service"
|
||||
size="medium"
|
||||
type="text"
|
||||
:style="{
|
||||
padding: 0,
|
||||
margin: '0 10px 0 10px',
|
||||
color: ['apply','agree','ok','success'].includes(buttonItem.code) ? 'green' : 'red'
|
||||
}"
|
||||
@click="onNodeOperationClick(buttonItem)"
|
||||
>
|
||||
{{ buttonItem.title }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 流水线状态为success,且有需要满足条件的操作按钮 -->
|
||||
<div v-else-if="templateData.status === 'success' && templateData.buttons && templateData.buttons.length > 0">
|
||||
<div
|
||||
v-else-if="pipelineData.currentNodeStatus === statusColorConst.success.code && pipelineData.buttons && pipelineData.buttons.length > 0"
|
||||
>
|
||||
<div class="box-buttons">
|
||||
<el-button
|
||||
v-for="buttonItem in templateData.buttons"
|
||||
v-for="buttonItem in pipelineData.buttons"
|
||||
:key="buttonItem.service"
|
||||
size="medium"
|
||||
type="text"
|
||||
@@ -108,7 +91,28 @@
|
||||
padding: 0,
|
||||
margin: '0 10px 0 10px',
|
||||
color: ['apply','agree','ok','success'].includes(buttonItem.code) ? 'green' : 'red',
|
||||
display: ['success', 'fail'].includes(buttonItem.code) && templateData.status === buttonItem.code ? '' : 'none'
|
||||
display: ['success', 'fail'].includes(buttonItem.code) && pipelineData.status === buttonItem.code ? '' : 'none'
|
||||
}"
|
||||
@click="onNodeOperationClick(buttonItem)"
|
||||
>
|
||||
{{ buttonItem.title }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 流水线状态非pending,且有操作按钮 -->
|
||||
<div
|
||||
v-else-if="pipelineData.currentNodeStatus !== statusColorConst.pending.code && pipelineData.buttons && pipelineData.buttons.length > 0"
|
||||
>
|
||||
<div class="box-buttons">
|
||||
<el-button
|
||||
v-for="buttonItem in pipelineData.buttons"
|
||||
:key="buttonItem.service"
|
||||
size="medium"
|
||||
type="text"
|
||||
:style="{
|
||||
padding: 0,
|
||||
margin: '0 10px 0 10px',
|
||||
color: ['apply','agree','ok','success'].includes(buttonItem.code) ? 'green' : 'red'
|
||||
}"
|
||||
@click="onNodeOperationClick(buttonItem)"
|
||||
>
|
||||
@@ -128,7 +132,7 @@
|
||||
</el-col>
|
||||
<div style="clear: both" />
|
||||
</el-row>
|
||||
<cute-preview-drawer ref="detailDrawer" :title="templateData.name">
|
||||
<cute-preview-drawer ref="detailDrawer" :title="pipelineData.name">
|
||||
<div style="padding: 20px">
|
||||
这里是明细
|
||||
</div>
|
||||
@@ -155,28 +159,26 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
statusColorConst: {
|
||||
pending: { label: '未开始', color: '#C0C4CC' },
|
||||
running: { label: '运行中', color: '#67C23A' },
|
||||
success: { label: '执行成功', color: '#67C23A' },
|
||||
fail: { label: '执行失败', color: '#F56C6C' }
|
||||
pending: { code: 'pending', label: '未开始', color: '#C0C4CC' },
|
||||
running: { code: 'running', label: '运行中', color: '#67C23A' },
|
||||
success: { code: 'success', label: '执行成功', color: '#67C23A' },
|
||||
fail: { code: 'fail', label: '执行失败', color: '#F56C6C' }
|
||||
},
|
||||
executeTimeStr: ''
|
||||
executeTimeStr: '',
|
||||
pipelineData: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
templateData: {
|
||||
handler(newVal, oldVal) {
|
||||
this.pipelineData = { ...newVal }
|
||||
this.executeTimeStr = this.renderDateTimeStr(newVal)
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
// computed: {
|
||||
// formatExecuteTimeStr() {
|
||||
// return this.renderDateTimeStr(this.templateData)
|
||||
// }
|
||||
// },
|
||||
mounted() {
|
||||
this.pipelineData = this.templateData
|
||||
this.executeTimeStr = this.renderDateTimeStr(this.templateData)
|
||||
},
|
||||
methods: {
|
||||
@@ -189,7 +191,20 @@ export default {
|
||||
case 'running':
|
||||
case 'success':
|
||||
case 'fail':
|
||||
executeTimeStr = this.formatTimeDifference(newVal.startTime, newVal.updateTime)
|
||||
if (newVal.currentNodeStatus !== this.statusColorConst.pending.code) {
|
||||
if (newVal.startTime && newVal.finishTime) {
|
||||
executeTimeStr = this.formatTimeDifference(newVal.startTime, newVal.finishTime)
|
||||
return executeTimeStr
|
||||
}
|
||||
if (newVal.startTime) {
|
||||
executeTimeStr = this.formatTimeDifference(newVal.startTime, new Date())
|
||||
return executeTimeStr
|
||||
}
|
||||
if (newVal.createTime) {
|
||||
executeTimeStr = this.formatTimeDifference(newVal.createTime, new Date())
|
||||
return executeTimeStr
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
executeTimeStr = ''
|
||||
@@ -223,10 +238,11 @@ export default {
|
||||
if (seconds >= 0) {
|
||||
return `${seconds} 秒`
|
||||
}
|
||||
return `${diffInMilliseconds} 毫秒`
|
||||
},
|
||||
onCurrentNodeClick() {
|
||||
const templateData = this.templateData || { click: false }
|
||||
if (!templateData.click) {
|
||||
const pipelineData = this.pipelineData || { click: false }
|
||||
if (!pipelineData.click) {
|
||||
console.warn('当前流水线节点不支持查看明细')
|
||||
return
|
||||
}
|
||||
@@ -234,12 +250,12 @@ export default {
|
||||
this.$refs.detailDrawer.show()
|
||||
},
|
||||
onNodeRetryClick() {
|
||||
const templateData = this.templateData || { retry: false }
|
||||
if (!templateData.retry) {
|
||||
const pipelineData = this.pipelineData || { retry: false }
|
||||
if (!pipelineData.retry) {
|
||||
console.warn('当前流水线节点不支持重试')
|
||||
return
|
||||
}
|
||||
console.error('templateData', this.templateData)
|
||||
console.error('pipelineData', this.pipelineData)
|
||||
},
|
||||
onNodeOperationClick(buttonInfo) {
|
||||
console.error('buttonInfo', buttonInfo)
|
||||
@@ -277,7 +293,7 @@ export default {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
font-size: 18px;
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-divider>演示:运行中</el-divider>
|
||||
<el-divider>演示:动态流水线 <el-button type="primary" @click="startPipelineTest">启动流水线</el-button></el-divider>
|
||||
<div class="box-pipeline">
|
||||
<div v-if="dataList1 && dataList1.length > 0" class="box-pipeline-content">
|
||||
<div v-if="dynamicList && dynamicList.length > 0" class="box-pipeline-content">
|
||||
<cute-pipeline-node
|
||||
v-for="(template, index) in dataList1"
|
||||
v-for="(template, index) in dynamicList"
|
||||
:key="template.code"
|
||||
:template-data="dataList1[index]"
|
||||
:template-data.sync="dynamicList[index]"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="box-pipeline-content">
|
||||
<cute-pipeline-node
|
||||
v-for="(template, index) in templateList"
|
||||
:key="template.code"
|
||||
:template-data="dataList1[index]"
|
||||
:template-data.sync="templateList[index]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -23,14 +23,14 @@
|
||||
<cute-pipeline-node
|
||||
v-for="(template, index) in dataList2"
|
||||
:key="template.code"
|
||||
:template-data="dataList2[index]"
|
||||
:template-data.sync="dataList2[index]"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="box-pipeline-content">
|
||||
<cute-pipeline-node
|
||||
v-for="(template, index) in templateList"
|
||||
:key="template.code"
|
||||
:template-data="dataList2[index]"
|
||||
:template-data.sync="templateList[index]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,14 +40,14 @@
|
||||
<cute-pipeline-node
|
||||
v-for="(template, index) in dataList3"
|
||||
:key="template.code"
|
||||
:template-data="dataList3[index]"
|
||||
:template-data.sync="dataList3[index]"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="box-pipeline-content">
|
||||
<cute-pipeline-node
|
||||
v-for="(template, index) in templateList"
|
||||
:key="template.code"
|
||||
:template-data="dataList3[index]"
|
||||
:template-data.sync="templateList[index]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,6 +58,7 @@
|
||||
|
||||
import CutePipelineNode from '@/views/components/dev/CutePipelineNode'
|
||||
import { formatDateTimeStr } from '@/utils'
|
||||
import { startPipeline, queryLastPipelineDetail } from '@/api/devops/pipeline'
|
||||
|
||||
export default {
|
||||
name: 'CutePipelineNodeDemo',
|
||||
@@ -154,147 +155,6 @@ export default {
|
||||
click: false,
|
||||
retry: true
|
||||
}],
|
||||
// 运行中
|
||||
dataList1: [
|
||||
{
|
||||
code: 'node_init',
|
||||
type: 'service',
|
||||
name: '初始化',
|
||||
createTime: '2025-07-17 21:12:01',
|
||||
updateTime: '2025-07-17 21:13:01',
|
||||
startTime: '2025-07-17 21:13:01',
|
||||
currentNodeMsg: '执行成功',
|
||||
currentNodeStatus: 'success',
|
||||
status: 'success'
|
||||
},
|
||||
{
|
||||
code: 'node_merge_branch',
|
||||
type: 'service',
|
||||
name: '合并代码',
|
||||
click: true,
|
||||
retry: true,
|
||||
createTime: '2025-07-17 21:13:01',
|
||||
updateTime: '2025-07-17 21:18:01',
|
||||
startTime: '2025-07-17 21:13:01',
|
||||
currentNodeMsg: '执行成功',
|
||||
currentNodeStatus: 'success',
|
||||
status: 'success'
|
||||
},
|
||||
{
|
||||
code: 'node_build_java',
|
||||
type: 'service',
|
||||
name: '构建',
|
||||
click: true,
|
||||
retry: true,
|
||||
detailType: 'gitlab',
|
||||
parameters: {
|
||||
pipeline: 'pipeline-backend'
|
||||
},
|
||||
createTime: '2025-07-17 21:18:01',
|
||||
updateTime: '2025-07-17 21:28:01',
|
||||
startTime: '2025-07-17 21:18:01',
|
||||
currentNodeMsg: '执行成功',
|
||||
currentNodeStatus: 'success',
|
||||
status: 'success'
|
||||
},
|
||||
{
|
||||
code: 'node_image_scan',
|
||||
type: 'service',
|
||||
name: '镜像扫描',
|
||||
click: true,
|
||||
retry: true,
|
||||
detailType: 'gitlab',
|
||||
parameters: {
|
||||
pipeline: 'pipeline-image-scan'
|
||||
},
|
||||
createTime: '2025-07-17 21:28:01',
|
||||
updateTime: '2025-07-17 21:28:21',
|
||||
startTime: '2025-07-17 21:28:01',
|
||||
currentNodeMsg: '执行成功',
|
||||
currentNodeStatus: 'success',
|
||||
status: 'success'
|
||||
},
|
||||
{
|
||||
code: 'node_approve_deploy',
|
||||
type: 'service',
|
||||
name: '部署审批',
|
||||
click: false,
|
||||
retry: true,
|
||||
buttons: [
|
||||
{
|
||||
type: 'execute',
|
||||
title: '同意',
|
||||
code: 'agree',
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
type: 'execute',
|
||||
title: '拒绝',
|
||||
code: 'refuse',
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
createTime: '2025-07-17 21:28:21',
|
||||
updateTime: '2025-07-17 21:28:21',
|
||||
startTime: '2025-07-17 21:28:21',
|
||||
currentNodeMsg: '等待人工审批',
|
||||
currentNodeStatus: 'running',
|
||||
status: 'running'
|
||||
},
|
||||
{
|
||||
code: 'node_deploy_java',
|
||||
type: 'service',
|
||||
name: '部署',
|
||||
click: true,
|
||||
retry: false,
|
||||
buttons: [{
|
||||
type: 'link',
|
||||
title: '查看部署详情',
|
||||
code: 'success',
|
||||
parameters: {
|
||||
'isBlank': true
|
||||
}
|
||||
}],
|
||||
createTime: '2025-07-17 21:28:21',
|
||||
updateTime: '2025-07-17 21:28:21',
|
||||
startTime: null,
|
||||
currentNodeMsg: '待执行',
|
||||
currentNodeStatus: 'pending',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
code: 'node_merge_confirm',
|
||||
type: 'service',
|
||||
name: '合并确认',
|
||||
click: false,
|
||||
retry: false,
|
||||
buttons: [{
|
||||
type: 'execute',
|
||||
title: '通过',
|
||||
code: 'agree',
|
||||
parameters: {}
|
||||
}],
|
||||
createTime: '2025-07-17 21:28:21',
|
||||
updateTime: '2025-07-17 21:28:21',
|
||||
startTime: null,
|
||||
currentNodeMsg: '待执行',
|
||||
currentNodeStatus: 'pending',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
code: 'node_merge_master',
|
||||
type: 'service',
|
||||
name: '合并回Master',
|
||||
click: false,
|
||||
retry: true,
|
||||
createTime: '2025-07-17 21:28:21',
|
||||
updateTime: '2025-07-17 21:28:21',
|
||||
startTime: null,
|
||||
currentNodeMsg: '待执行',
|
||||
currentNodeStatus: 'pending',
|
||||
status: 'pending'
|
||||
}
|
||||
],
|
||||
// 成功
|
||||
dataList2: [
|
||||
{
|
||||
@@ -576,21 +436,18 @@ export default {
|
||||
currentNodeStatus: 'pending',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
],
|
||||
// 动态流水线
|
||||
dynamicHook: null,
|
||||
dynamicList: [],
|
||||
dynamicInstanceId: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setInterval(this.refreshData, 1000)
|
||||
this.refreshData()
|
||||
},
|
||||
methods: {
|
||||
refreshData() {
|
||||
// 运行中
|
||||
for (const datum of this.dataList1) {
|
||||
if (datum.status === 'running') {
|
||||
datum.updateTime = formatDateTimeStr(new Date())
|
||||
}
|
||||
}
|
||||
this.dataList1 = [...this.dataList1]
|
||||
// 成功
|
||||
for (const datum of this.dataList2) {
|
||||
if (datum.status === 'running') {
|
||||
@@ -605,6 +462,22 @@ export default {
|
||||
}
|
||||
}
|
||||
this.dataList3 = [...this.dataList3]
|
||||
},
|
||||
startPipelineTest() {
|
||||
const that = this
|
||||
if (that.dynamicHook) {
|
||||
clearInterval(that.dynamicHook)
|
||||
}
|
||||
startPipeline().then((instanceId) => {
|
||||
that.dynamicInstanceId = instanceId
|
||||
setTimeout(() => {
|
||||
that.dynamicHook = setInterval(() => {
|
||||
queryLastPipelineDetail(that.dynamicInstanceId).then((data) => {
|
||||
that.dynamicList = data
|
||||
})
|
||||
}, 1000)
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user