feature: 子主流程嵌套的子流程也要创建执行历史

This commit is contained in:
charlesxie
2022-06-16 18:31:45 +08:00
parent 4d9e6bff85
commit 194c649172
13 changed files with 1012 additions and 193 deletions

View File

@@ -2,8 +2,13 @@
![image](https://user-images.githubusercontent.com/29135056/155830656-968f5881-5729-4347-94fc-b5a657ea9725.png)
## todo list
- [ ] 子流程状态显示
- [ ] 主流程嵌套的子流程也要创建执行历史
- [x] 子流程状态显示
- [x] 主流程嵌套的子流程也要创建执行历史
- [ ] 任务管理
- [ ] 定时任务和周期任务
- [ ] 暂停,停止,跳过等人工干预操作
- [ ] 节点重试功能
-
## install tips
sudo apt-get install libmysqlclient-dev
python3-dev

View File

@@ -0,0 +1,62 @@
# Generated by Django 2.2.6 on 2022-06-16 16:14
import datetime
from django.db import migrations, models
import django.db.models.deletion
import django_mysql.models
class Migration(migrations.Migration):
dependencies = [
('flow', '0004_auto_20220226_1202'),
]
operations = [
migrations.CreateModel(
name='SubProcessRun',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='作业名称')),
('description', models.CharField(blank=True, max_length=255, null=True, verbose_name='作业描述')),
('run_type', models.CharField(max_length=32, verbose_name='调度类型')),
('gateways', django_mysql.models.JSONField(default=dict, verbose_name='网关信息')),
('constants', django_mysql.models.JSONField(default=dict, verbose_name='内部变量信息')),
('dag', django_mysql.models.JSONField(default=dict, verbose_name='DAG')),
('create_by', models.CharField(max_length=64, null=True, verbose_name='创建者')),
('create_time', models.DateTimeField(default=datetime.datetime.now, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('update_by', models.CharField(max_length=64, null=True, verbose_name='修改人')),
('root_id', models.CharField(max_length=255, verbose_name='根节点uuid')),
('process', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sub_run', to='flow.Process')),
('process_run', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sub', to='flow.Process')),
],
),
migrations.CreateModel(
name='SubNodeRun',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='节点名称')),
('uuid', models.CharField(max_length=255, unique=True, verbose_name='UUID')),
('description', models.CharField(blank=True, max_length=255, null=True, verbose_name='节点描述')),
('show', models.BooleanField(default=True, verbose_name='是否显示')),
('top', models.IntegerField(default=300)),
('left', models.IntegerField(default=300)),
('ico', models.CharField(blank=True, max_length=64, null=True, verbose_name='icon')),
('fail_retry_count', models.IntegerField(default=0, verbose_name='失败重试次数')),
('fail_offset', models.IntegerField(default=0, verbose_name='失败重试间隔')),
('fail_offset_unit', models.CharField(choices=[('seconds', ''), ('hours', ''), ('minutes', '')], max_length=32, verbose_name='重试间隔单位')),
('node_type', models.IntegerField(default=2)),
('component_code', models.CharField(max_length=255, verbose_name='插件名称')),
('is_skip_fail', models.BooleanField(default=False, verbose_name='忽略失败')),
('is_timeout_alarm', models.BooleanField(default=False, verbose_name='超时告警')),
('inputs', django_mysql.models.JSONField(default=dict, verbose_name='输入参数')),
('outputs', django_mysql.models.JSONField(default=dict, verbose_name='输出参数')),
('content', models.IntegerField(default=0, verbose_name='模板id')),
('subprocess_run', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sub_nodes_run', to='flow.SubProcessRun')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 2.2.6 on 2022-06-16 16:16
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('flow', '0005_subnoderun_subprocessrun'),
]
operations = [
migrations.AlterField(
model_name='noderun',
name='process_run',
field=models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='nodes_run', to='flow.ProcessRun'),
),
migrations.AlterField(
model_name='subnoderun',
name='subprocess_run',
field=models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sub_nodes_run', to='flow.SubProcessRun'),
),
]

View File

@@ -98,8 +98,37 @@ class ProcessRun(models.Model):
root_id = models.CharField("根节点uuid", max_length=255)
class SubProcessRun(models.Model):
process_run = models.ForeignKey(Process, on_delete=models.CASCADE, null=True, db_constraint=False,
related_name="sub")
process = models.ForeignKey(Process, on_delete=models.SET_NULL, null=True, db_constraint=False,
related_name="sub_run")
name = models.CharField("作业名称", max_length=255, blank=False, null=False)
description = models.CharField("作业描述", max_length=255, blank=True, null=True)
run_type = models.CharField("调度类型", max_length=32)
gateways = JSONField("网关信息", default=dict)
constants = JSONField("内部变量信息", default=dict)
dag = JSONField("DAG", default=dict)
create_by = models.CharField("创建者", max_length=64, null=True)
create_time = models.DateTimeField("创建时间", default=datetime.now)
update_time = models.DateTimeField("修改时间", auto_now=True)
update_by = models.CharField("修改人", max_length=64, null=True)
root_id = models.CharField("根节点uuid", max_length=255)
class SubNodeRun(BaseNode):
subprocess_run = models.ForeignKey(SubProcessRun, on_delete=models.CASCADE, null=True, db_constraint=False,
related_name="sub_nodes_run")
@staticmethod
def field_names():
return [field.name for field in NodeRun._meta.get_fields() if field.name not in ["id"]]
class NodeRun(BaseNode):
process_run = models.ForeignKey(ProcessRun, on_delete=models.SET_NULL, null=True, db_constraint=False,
process_run = models.ForeignKey(ProcessRun, on_delete=models.CASCADE, null=True, db_constraint=False,
related_name="nodes_run")
@staticmethod

View File

@@ -8,7 +8,7 @@ from pipeline.eri.runtime import BambooDjangoRuntime
from rest_framework import serializers
from applications.flow.constants import PIPELINE_STATE_TO_FLOW_STATE
from applications.flow.models import Process, Node, ProcessRun, NodeRun, NodeTemplate
from applications.flow.models import Process, Node, ProcessRun, NodeRun, NodeTemplate, SubProcessRun, SubNodeRun
from applications.utils.uuid_helper import get_uuid
@@ -161,6 +161,23 @@ class ListProcessRunViewSetsSerializer(serializers.ModelSerializer):
return process_state
class ListSubProcessRunViewSetsSerializer(serializers.ModelSerializer):
state = serializers.SerializerMethodField()
class Meta:
model = SubProcessRun
fields = "__all__"
def get_state(self, obj):
runtime = BambooDjangoRuntime()
process_info = api.get_pipeline_states(runtime, root_id=obj.root_id)
try:
process_state = PIPELINE_STATE_TO_FLOW_STATE.get(process_info.data[obj.root_id]["state"])
except Exception:
process_state = "error"
return process_state
class RetrieveProcessViewSetsSerializer(serializers.ModelSerializer):
pipeline_tree = serializers.SerializerMethodField()
@@ -239,7 +256,7 @@ class RetrieveProcessRunViewSetsSerializer(serializers.ModelSerializer):
outputs = output_data.data.get("outputs", "")
if node["node_type"] == 3:
# todo先简单判断node有failprocess就为fail
if State.objects.filter(parent_id=node["uuid"],name="FAILED").exists():
if State.objects.filter(parent_id=node["uuid"], name="FAILED").exists():
flow_state = "fail"
# todo先简单判断node有failprocess就为fail
if flow_state == "fail":
@@ -271,6 +288,65 @@ class RetrieveProcessRunViewSetsSerializer(serializers.ModelSerializer):
fields = ("id", "name", "description", "run_type", "pipeline_tree")
class RetrieveSubProcessRunViewSetsSerializer(serializers.ModelSerializer):
pipeline_tree = serializers.SerializerMethodField()
def get_pipeline_tree(self, obj):
lines = []
nodes = []
for _from, to_list in obj.dag.items():
for _to in to_list:
lines.append({
"from": _from,
"to": _to
})
runtime = BambooDjangoRuntime()
process_info = api.get_pipeline_states(runtime, root_id=obj.root_id)
process_state = PIPELINE_STATE_TO_FLOW_STATE.get(process_info.data[obj.root_id]["state"])
state_map = process_info.data[obj.root_id]["children"]
node_list = SubNodeRun.objects.filter(subprocess_run_id=obj.id).values()
for node in node_list:
pipeline_state = state_map.get(node["uuid"], {}).get("state", "READY")
flow_state = PIPELINE_STATE_TO_FLOW_STATE[pipeline_state]
outputs = ""
# print(flow_state)
if node["node_type"] not in [0, 1] and flow_state not in ["wait"]:
output_data = api.get_execution_data_outputs(runtime, node_id=node["uuid"])
outputs = output_data.data.get("outputs", "")
if node["node_type"] == 3:
# todo先简单判断node有failprocess就为fail
if State.objects.filter(parent_id=node["uuid"], name="FAILED").exists():
flow_state = "fail"
# todo先简单判断node有failprocess就为fail
if flow_state == "fail":
process_state = "fail"
nodes.append({"show": node["show"],
"top": node["top"],
"left": node["left"],
"ico": node["ico"],
"type": node["node_type"],
"name": node["name"],
"state": flow_state,
"content": node["content"],
"node_data": {
"inputs": node["inputs"],
"outputs": outputs,
"run_mark": 0,
"node_name": node["name"],
"description": node["description"],
"fail_retry_count": node["fail_retry_count"],
"fail_offset": node["fail_offset"],
"fail_offset_unit": node["fail_offset_unit"],
"is_skip_fail": node["is_skip_fail"],
"is_timeout_alarm": node["is_timeout_alarm"]},
"uuid": node["uuid"]})
return {"lines": lines, "nodes": nodes, "process_state": process_state}
class Meta:
model = SubProcessRun
fields = ("id", "name", "description", "run_type", "pipeline_tree")
class ExecuteProcessSerializer(serializers.Serializer):
process_id = serializers.IntegerField(required=True)

View File

@@ -5,6 +5,7 @@ from . import views
flow_router = DefaultRouter()
flow_router.register(r"flow", viewset=views.ProcessViewSets, base_name="flow")
flow_router.register(r"run", viewset=views.ProcessRunViewSets, base_name="run")
flow_router.register(r"sub_run", viewset=views.SubProcessRunViewSets, base_name="sub_run")
flow_router.register(r"test", viewset=views.TestViewSets, base_name="test")
node_router = DefaultRouter()

View File

@@ -1,8 +1,9 @@
from applications.flow.models import ProcessRun, NodeRun, Process
from applications.flow.models import ProcessRun, NodeRun, Process, Node, SubProcessRun, SubNodeRun
from applications.utils.dag_helper import PipelineBuilder, instance_dag
def build_and_create_process(process_id):
"""构建pipeline和创建运行时数据"""
p_builder = PipelineBuilder(process_id)
pipeline = p_builder.build()
@@ -19,5 +20,24 @@ def build_and_create_process(process_id):
_node = {k: v for k, v in node.__dict__.items() if k in NodeRun.field_names()}
_node["uuid"] = process_run_uuid[pipeline_id].id
node_run_bulk.append(NodeRun(process_run=process_run, **_node))
if node.node_type == Node.SUB_PROCESS_NODE:
create_subprocess(node.content, process_run.id, process_run_uuid, pipeline["id"])
NodeRun.objects.bulk_create(node_run_bulk, batch_size=500)
return pipeline
def create_subprocess(process_id, process_run_id, process_run_uuid, root_id):
process = Process.objects.filter(id=process_id).first()
process_run_data = process.clone_data
process_run_data["dag"] = instance_dag(process_run_data["dag"], process_run_uuid)
process_run = SubProcessRun.objects.create(process_id=process_id, process_run_id=process_run_id, root_id=root_id,
**process_run_data)
subprocess_node_map = Node.objects.filter(process_id=process_id).in_bulk(field_name="uuid")
node_run_bulk = []
for pipeline_id, node in subprocess_node_map.items():
_node = {k: v for k, v in node.__dict__.items() if k in NodeRun.field_names()}
_node["uuid"] = process_run_uuid[pipeline_id].id
node_run_bulk.append(SubNodeRun(subprocess_run=process_run, **_node))
if node.node_type == Node.SUB_PROCESS_NODE:
create_subprocess(node.content, process_run_id, process_run_uuid, root_id)
SubNodeRun.objects.bulk_create(node_run_bulk, batch_size=500)

View File

@@ -12,10 +12,11 @@ from rest_framework.decorators import action
from rest_framework.response import Response
from applications.flow.filters import NodeTemplateFilter
from applications.flow.models import Process, Node, ProcessRun, NodeRun, NodeTemplate
from applications.flow.models import Process, Node, ProcessRun, NodeRun, NodeTemplate, SubProcessRun
from applications.flow.serializers import ProcessViewSetsSerializer, ListProcessViewSetsSerializer, \
RetrieveProcessViewSetsSerializer, ExecuteProcessSerializer, ListProcessRunViewSetsSerializer, \
RetrieveProcessRunViewSetsSerializer, NodeTemplateSerializer
RetrieveProcessRunViewSetsSerializer, NodeTemplateSerializer, ListSubProcessRunViewSetsSerializer, \
RetrieveSubProcessRunViewSetsSerializer
from applications.utils.dag_helper import DAG, instance_dag, PipelineBuilder
from component.drf.viewsets import GenericViewSet
@@ -45,7 +46,7 @@ class ProcessViewSets(mixins.ListModelMixin,
# 执行
runtime = BambooDjangoRuntime()
api.run_pipeline(runtime=runtime, pipeline=pipeline)
Process.objects.filter(id=process_id).update(total_run_count=F("total_run_count") + 1)
return Response({})
@@ -65,6 +66,18 @@ class ProcessRunViewSets(mixins.ListModelMixin,
return ExecuteProcessSerializer
class SubProcessRunViewSets(mixins.ListModelMixin,
mixins.RetrieveModelMixin,
GenericViewSet):
queryset = SubProcessRun.objects.order_by("-update_time")
def get_serializer_class(self):
if self.action == "list":
return ListSubProcessRunViewSetsSerializer
elif self.action == "retrieve":
return RetrieveSubProcessRunViewSetsSerializer
class TestViewSets(GenericViewSet):
def list(self, request, *args, **kwargs):
random_list = [1, 1, 1, 1, 1, 1, 1, 1, 1, 0]

View File

@@ -212,6 +212,7 @@ class PipelineBuilder:
self.instance = self.setup_instance()
def setup_instance(self):
"""将节点转换成bamboo实例"""
pipeline_instance = {}
for p_id, node in self.node_map.items():
if node.node_type == Node.START_NODE:
@@ -244,6 +245,8 @@ class PipelineBuilder:
p_builder = PipelineBuilder(process_id)
pipeline = p_builder.build(is_subprocess=True)
pipeline_instance[p_id] = pipeline
# 子流程的pid一并加入pipeline_instance
pipeline_instance.update(p_builder.instance)
else:
act = ServiceActivity(component_code="http_request")
act.component.inputs.inputs = Var(type=Var.PLAIN, value=node.inputs)

View File

@@ -2,10 +2,10 @@ import {GET, POST, reUrl} from '../../axiosconfig/axiosconfig'
export default {
list: function(params) {
return GET(reUrl + '/node_run/', params)
return GET(reUrl + '/process/sub_run/', params)
},
retrieve: function(id, params) {
return GET(reUrl + '/node_run/' + JSON.stringify(id) + '/', params)
return GET(reUrl + '/process/sub_run/' + JSON.stringify(id) + '/', params)
},
control: function(params) {
return POST(reUrl + '/node_run/control/', params)

View File

@@ -1,168 +1,704 @@
<template>
<div id="jobDetail" v-bkloading="{ isLoading: jobDetailLoading, zIndex: 10 }">
<div id="jobFlowViewDetail" v-bkloading="{ isLoading: formLoading, zIndex: 999999 }">
<div class="box">
<p class="title">基本信息</p>
<bk-container>
<bk-form :label-width="130">
<bk-row>
<bk-col :span="6">
<bk-form-item label="作业名称:">{{form.name}}</bk-form-item>
<bk-form-item label="作业名称:">{{ form.name }}</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="作业状态:">
<span v-if="form.state !== ''">{{stateList[stateList.findIndex(e => e.name === form.state)].label}}</span>
<bk-form-item label="作业状态:"><span
v-if="form.hasOwnProperty('state')">{{ stateList[stateList.findIndex(e => e.name === form.state)].label }}</span>
</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="启动人:">{{form.executor}}</bk-form-item>
<bk-form-item label="启动人:">{{ form.executor }}</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="Agent:">{{form.station}}</bk-form-item>
<bk-form-item label="跑批系统:">{{ form.category_name }}</bk-form-item>
</bk-col>
</bk-row>
<bk-row>
<bk-col :span="6">
<bk-form-item label="计划开始时间:">{{form.eta}}</bk-form-item>
<bk-form-item label="计划开始时间:">{{ form.eta }}</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="实际开始时间:">{{form.start_time}}</bk-form-item>
<bk-form-item label="实际开始时间:">{{ form.start_time }}</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="完成时间:">{{form.end_time}}</bk-form-item>
<bk-form-item label="完成时间:">{{ form.end_time }}</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="总共耗时:">{{form.total_time}}</bk-form-item>
<bk-form-item label="总共耗时:">{{ form.total_time }}</bk-form-item>
</bk-col>
</bk-row>
</bk-form>
</bk-container>
</div>
<div class="box">
<p class="title">执行日志</p>
<editor :height="'200px'" ref="editorLog" :codes="form.log" :read-only="true" :language="'shell'"></editor>
<!-- <bk-input :type="'textarea'" :rows="10" ext-cls="custom-textarea" v-model="form.log" :disabled="true"></bk-input> -->
<p class="title">执行详情</p>
<div id="content" v-bkloading="{ isLoading: mainLoading, zIndex: 999 }">
<div class="left-statusList">
<statusList style="position: absolute;left: 20px;top: 15px;"></statusList>
</div>
<div class="right-canvas">
<div id="main" ref="main"></div>
</div>
</div>
</div>
<div class="box">
<p class="title">执行脚本</p>
<editor :height="'200px'" ref="editorScript" :codes="form.script_content" :read-only="true" :language="'shell'"></editor>
<!-- <bk-input :type="'textarea'" :rows="10" ext-cls="custom-textarea" v-model="form.script_content" :disabled="true"></bk-input> -->
</div>
<div class="box">
<p class="title">前置命令检测</p>
<editor :height="'200px'" ref="editorPrecommd" :read-only="true" :language="'json'"></editor>
<div class="node-drawer">
<bk-sideslider :is-show.sync="nodeDrawer.show" :quick-close="true" :title="nodeDrawer.title"
:width="nodeDrawer.width" ext-cls="custom-sidelider">
<node-info slot="content" :node-data="nodeData" :key="nodeSliderKey">
</node-info>
</bk-sideslider>
</div>
<bk-dialog title="连线模式选择"
v-model="flowModeDialog.show"
:confirm-fn="handleFlowAddEdgeConfirm"
ext-cls="add-mode-dialog"
:mask-close="false"
header-position="left">
<add-mode-dialog :key="flowModeDialogKey" ref="addModeDialog"></add-mode-dialog>
<bk-dialog v-model="flowModeDialog.childDialog.show"
:mask-close="false"
:width="flowModeDialog.childDialog.width"
header-position="left"
:render-directive="'if'"
:position="{ top: 50 }"
ext-cls="pre-flow-canvas-dialog"
:confirm-fn="handlePreFlowNOdeAddConfirm"
:show-footer="flowModeDialog.childDialog.footerShow">
<div slot="header">
<span style="color: #313237;">当前作业流{{ flowModeDialog.childDialog.title }}</span>
<span class="iconfont icon-mianxingtubiao-wenti"
style="margin-left: 4px;color: #979BA5;font-size: 16px;"
v-bk-tooltips="flowModeTipConfig"></span>
</div>
<pre-flow-canvas :options="flowModeDialog.curObj" :pre-edges="flowModeDialog.preEdges"
ref="preFlowCanvas"></pre-flow-canvas>
</bk-dialog>
</bk-dialog>
</div>
</template>
<script>
import editor from '@/components/monacoEditor'
import {
deepClone, getUUID
} from '../../../common/util.js'
import registerFactory from '@/components/graph/graph.js'
import G6 from '@antv/g6'
import statusList from './job_flow_view_detail/statusList.vue'
import nodeInfo from './job_flow_view_detail/nodeInfo.vue'
import addModeDialog from '@/views/job_flow_mgmt/single_job_flow/addModeDialog.vue'
import preFlowCanvas from '@/views/job_flow_mgmt/single_job_flow/preFlowCanvas.vue'
export default {
components: {
editor
statusList,
nodeInfo,
addModeDialog, // 前置作业流连线模式选择弹窗
preFlowCanvas // 前置作业流详情画布
},
data() {
return {
timer: null, // 轮询定时器
jobDetailLoading: false,
form: {
name: '', // 作业名称
state: '', // 状态
executor: '', // 启动人
station: '', // agent
eta: '', // 计划开始时间
start_time: '', // 实际开始时间
end_time: '', // 完成时间
total_time: '', // 总共耗时
log: '', // 执行日志
script_content: '', // 执行脚本
upstream_nodes: [], // 先行作业/作业流
downstream_nodes: [] // 后续作业/作业流
formLoading: false,
nodeSliderKey: 0,
flowModeDialogKey: 0, // 前置作业流连线弹窗组件key
form: {},
graph: null,
mainLoading: false,
opreateFlag: false,
tooltip: null, // 内容超出提示
menu: null, // 右键菜单
cfg: {}, // 配置项
nodeDrawer: {
title: '',
show: false,
width: 600
},
stateList: [{
id: 1,
name: 'wait',
label: '等待'
},
{
id: 2,
name: 'run',
label: '正在执行'
},
{
id: 3,
name: 'fail',
label: '失败'
},
{
id: 4,
name: 'error',
label: '错误'
},
{
id: 5,
name: 'success',
label: '成功'
},
{
id: 6,
name: 'pause',
label: '挂起'
},
{
id: 7,
name: 'stop',
label: '终止'
},
{
id: 8,
name: 'cancel',
label: '取消'
},
{
id: 9,
name: 'need_confirm',
label: '待复核'
},
{
id: 10,
name: 'ignore',
label: '忽略'
}
nodeData: {},
timer: null,
flowModeDialog: { // 前置作业流连线模式选择弹窗
show: false,
curObj: {}, // 当前前置作业流的信息
preEdges: [], // 当前前置作业流节点的出线集
childDialog: { // 当前前置作业流节点的详情弹框
footerShow: false,
show: false,
width: 960,
title: ''
}
},
flowModeTipConfig: {
content: '选择前置作业流中的某个节点作为前置依赖连线,不可重复选择节点连线,不可选择该作业流中的前置作业流节点!',
placement: 'right',
width: 300,
zIndex: 999999
// delay: [0, 60000]
},
stateList: [
{
id: 1,
name: 'wait',
label: '等待'
},
{
id: 2,
name: 'run',
label: '正在执行'
},
{
id: 3,
name: 'fail',
label: '失败'
},
{
id: 4,
name: 'error',
label: '错误'
},
{
id: 5,
name: 'success',
label: '成功'
},
{
id: 6,
name: 'pause',
label: '挂起'
},
{
id: 7,
name: 'cancel',
label: '取消'
},
{
id: 8,
name: 'positive',
label: '就绪'
},
{
id: 9,
name: 'stop',
label: '终止'
},
{
id: 10,
name: 'need_confirm',
label: '待审核'
},
{
id: 11,
name: 'ignore',
label: '忽略'
},
{
id: 12,
name: 'exists_need_confirm',
label: '正在执行(存在审核)'
},
{
id: 13,
name: 'exists_error',
label: '正在执行(存在错误)'
},
{
id: 14,
name: 'exists_fail',
label: '正在执行(存在失败)'
},
{
id: 15,
name: 'exists_stop',
label: '正在执行(存在终止)'
},
{
id: 16,
name: 'exists_pause',
label: '正在执行(存在挂起)'
}
]
}
},
created() {
// 首屏刷新
this.handleLoad(true)
// 不需要清空画布,首屏刷新
this.handleLoad(false, true)
},
mounted() {
// 创建画布
this.$nextTick(() => {
this.createGraphic()
this.initGraphEvent()
})
// 轮询画布
this.timer = setInterval(() => {
// 轮询刷新,非首屏
this.handleLoad(false)
}, 10000)
// 不需要清空画布,非首屏刷新
this.handleLoad(false, false)
}, 3000)
window.addEventListener('resize', this.handleChangeCavasSize, false)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleChangeCavasSize, false)
this.graph.destroy()
clearInterval(this.timer)
},
methods: {
handleLoad(first = false) {
if (first) {
this.jobDetailLoading = true
// 处理改变画布大小
handleChangeCavasSize() {
this.graph.changeSize(this.$refs.main.clientWidth, 550)
this.graph.fitView([20, 30, 30, 80])
},
handlePreFlowNOdeAddConfirm() {
if (!this.$refs.preFlowCanvas.currentChooseNode) {
this.$cwMessage('当前作业节点未选择,至少选择一个作业节点!', 'warning')
} else {
const _this = this
setTimeout(() => {
const label = `${_this.$refs.preFlowCanvas.currentChooseNode.name}${_this.flowModeDialog.curObj.targetNode.getModel().name}`
this.graph.addItem('edge', {
id: getUUID(32, 16),
source: _this.flowModeDialog.curObj.sourceNode.get('id'),
target: _this.flowModeDialog.curObj.targetNode.get('id'),
label: label.length > 10 ? `${label.substr(0, 10)}...` : label,
rely_node: {
name: label,
label: label.length > 10 ? `${label.substr(0, 10)}...` : label,
content: _this.$refs.preFlowCanvas.currentChooseNode.content,
id: _this.$refs.preFlowCanvas.currentChooseNode.id
},
gateWay: {
name: '', // 分支名
expression: '' // 条件表达式
}
})
_this.flowModeDialog.childDialog.show = false
_this.flowModeDialog.show = false
}, 100)
}
},
handleFlowAddEdgeConfirm() {
if (this.$refs.addModeDialog.modeValue === 'flow') {
setTimeout(() => {
this.graph.addItem('edge', {
id: getUUID(32, 16),
source: this.flowModeDialog.curObj.sourceNode.get('id'),
target: this.flowModeDialog.curObj.targetNode.get('id'),
gateWay: {
name: '', // 分支名
expression: '' // 条件表达式
}
})
}, 100)
this.flowModeDialog.show = false
} else {
const edges = this.flowModeDialog.curObj.sourceNode.getEdges()
// 表明已有其他前置连线,收集前置节点连线
if (edges.length) {
this.flowModeDialog.preEdges = edges.filter(item => {
return item.getModel().hasOwnProperty('label')
})
}
this.flowModeDialog.childDialog.title = this.flowModeDialog.curObj.sourceNode.getModel().name
this.flowModeDialog.childDialog.footerShow = true
this.flowModeDialog.childDialog.show = true
}
},
handleOpenFlowDrawer(e) {
this.flowModeDialog.preEdges = []
const edges = e.item.getEdges()
console.log(1234, e.item.getModel())
// 表明已有其他前置连线,收集前置节点连线
if (edges.length) {
this.flowModeDialog.preEdges = edges.filter(item => {
return item.getModel().hasOwnProperty('label')
})
}
this.flowModeDialog.curObj = {sourceNode: e.item}
this.flowModeDialog.childDialog.title = e.item.getModel().name
this.flowModeDialog.childDialog.footerShow = false
this.flowModeDialog.childDialog.show = true
},
initGraphEvent() {
this.graph.on('node:click', e => {
const model = e.item.get('model')
// 开始节点,结束节点,作业流节点不做处理
console.log(model.nodeType)
if (model.nodeType === 0 || model.nodeType === 1) {
return false
}
if (model.nodeType === 3) {
this.handleOpenFlowDrawer(e)
} else {
this.nodeData = {
data: deepClone(model.node_data), // 深拷贝节点数据
log: model.log,
state: model.state,
script_content: model.script_content,
start_time: model.start_time,
end_time: model.end_time,
id: model.id
}
this.nodeSliderKey += 1
this.nodeDrawer.show = true
this.nodeDrawer.title = model.name
}
})
},
createGraphic() {
// 创建菜单
this.createMenu()
// 初始化配置项
this.initOption()
// 创建graph实例
this.graph = new G6.Graph(this.cfg)
},
renderCanvas(detail, first) {
if (first) {
this.mainLoading = true
}
const _this = this
setTimeout(() => {
const data = {
edges: _this.form.pipeline_tree.lines.map(line => {
return {
detail: detail,
id: getUUID(32, 16),
source: line.from,
target: line.to
}
}),
nodes: _this.form.pipeline_tree.nodes.map((node, index) => {
let style = {}
if (node.type === 0 || node.type === 1) {
style = {
fill: '#fff',
r: 24
}
} else {
style = {
width: 154,
height: 40,
radius: 20,
iconCfg: {
fill: '#3a84ff'
}
}
}
return {
...node,
detail: detail,
label: node.name.length > 8 ? `${node.name.substr(0, 8)}...` : node
.name,
name: node.name,
icon: node.ico,
id: node.hasOwnProperty('end_uuid') ? node.end_uuid : node.uuid,
jobId: node.id,
x: Number(node.left),
y: Number(node.top),
nodeType: node.type,
state: node.state,
type: (node.type === 0 || node.type === 1) ? 'circle-node'
: 'rect-node',
labelCfg: {
style: {
textAlign: (node.type === 0 || node.type === 1) ? 'center'
: 'left'
}
},
style: {
...style
}
}
})
}
_this.graph.read(data)
// _this.graph.fitCenter()
_this.mainLoading = false
}, 2000)
},
initOption() {
// 工厂函数注册自定义节点
this.cfg = registerFactory(G6, {
width: this.$refs.main.clientWidth,
height: 550,
animate: true, // Boolean可选切换布局时是否使用动画过度
maxZoom: 1, // 最大缩放比例
fitView: true,
// fitView: true,
// layout: {
// type: 'xxx'
// },
// layout: {
// type: 'dagre',
// rankdir: 'LR', // 可选,默认为图的中心
// align: 'DL', // 可选
// nodesep: 20, // 可选
// ranksep: 50, // 可选
// controlPoints: false, // 可选
// },
defaultNode: {
type: 'rect-node',
style: {
radius: 10
},
labelCfg: {
fontSize: 20
}
},
defaultEdge: {
type: 'polyline-edge', // 扩展了内置边, 有边的事件
// type: 'cubic-vertical-edge', // 扩展了内置边, 有边的事件
style: {
radius: 0, // 拐弯弧度
offset: 15, // 拐弯处距离节点的最小距离
stroke: '#aab7c3',
lineAppendWidth: 10, // 防止线太细没法点中
endArrow: {
path: 'M 0,0 L 4,3 L 3,0 L 4,-3 Z',
fill: '#aab7c3',
stroke: '#aab7c3'
},
zIndex: 999999
}
},
// 覆盖全局样式
nodeStateStyles: {
'nodeState:default': {
opacity: 1,
fill: '#fff',
stroke: '#DCDEE5',
labelCfg: {
style: {
fill: '#333333'
}
}
},
'nodeState:hover': {
opacity: 0.8
},
'nodeState:selected': {
opacity: 0.9,
stroke: 'rgb(58,132,255)',
labelCfg: {
style: {
fill: 'rgb(58,132,255)'
}
}
}
},
// linkCenter: true,
plugins: [this.tooltip, this.menu],
modes: {
// 允许拖拽画布、缩放画布、拖拽节点
default: [
'drag-canvas', // 官方内置的行为
'zoom-canvas',
'hover-node',
'drag-node',
'hover-edge'
// 'select-node'
]
}
})
},
// 创建菜单
createMenu() {
const _this = this
// 创建内容超出提示
this.tooltip = new G6.Tooltip({
offsetX: 20,
offsetY: -20,
itemTypes: ['node'],
// 自定义 tooltip 内容
getContent: (e) => {
const outDiv = document.createElement('div')
outDiv.style.width = 'fit-content'
outDiv.innerHTML = `<ul><li>${e.item.getModel().name}</li></ul>`
return outDiv
},
shouldBegin(e) {
const model = e.item.get('model')
// 触发方式只有在内容超出8个字符的情况下才触发
if (model.nodeType === 0 || model.nodeType === 1 || model.name.length <= 8) {
return false
}
return true
}
})
// 创建右键菜单
this.menu = new G6.Menu({
offsetX: 20,
offsetY: 20,
itemTypes: ['node'],
getContent(e) {
const model = e.item.get('model')
const outDiv = document.createElement('div')
outDiv.style.width = '60px'
outDiv.style.cursor = 'pointer'
outDiv.innerHTML = _this.renderRightMenu(model)
return outDiv
},
shouldBegin(e) {
const model = e.item.get('model')
// 触发方式,开始节点和结束节点或节点状态为成功的都不触发
if (model.nodeType === 0 || model.nodeType === 1 || model.state === 'ignore') {
return false
}
return true
},
handleMenuClick(target, item) {
const model = item.get('model')
const {
id
} = target
_this.handleOperation(id, model.jobId)
}
})
},
// 处理执行节点操作
handleOperation(str, id) {
const contentMap = {
'pause': {
preState: '等待',
content: '作业暂停执行,不会继续后面的执行',
width: 450
},
'resume': {
preState: '挂起',
content: '恢复挂起作业流',
width: 400
},
'stop': {
preState: '进行中',
content: '终止后不会继续后面的执行,并且无法恢复。会强制终止此作业',
width: 400
},
'cancel': {
preState: '除了正在执行',
content: '将作业状态置为取消,可以继续往下执行',
width: 400
},
'replay': {
preState: '已完成、错误、失败,终止、取消',
content: '复制一份该作业,并放入原作业流中。如果新的作业成功,那么对作业流就是成功了',
width: 650
},
'release': {
preState: '未执行、等待',
content: '释放此作业的被依赖关系(包括时间依赖)',
width: 400
},
'success': {
preState: '错误,失败,终止',
content: '针对错误,失败,终止的作业,设置为成功',
width: 400
},
'confirm': {
preState: '待复核',
content: '针对待复核的作业,设置为等待',
width: 400
}
}
this.$bkInfo({
type: 'primary',
title: `执行前状态:${contentMap[str].preState}`,
subTitle: `功能说明:${contentMap[str].content}`,
width: contentMap[str].width,
confirmLoading: false,
confirmFn: async() => {
this.mainLoading = true
// 解决操作执行过程中由于轮询接口渲染画布而导致mainLoading刷新。
// 加入opreateFlag保证轮询过程中不会刷新画布
this.opreateFlag = true
this.$api.nodeRun.control({
'event': str,
'ids': [id]
}).then(res => {
if (res.result) {
this.$cwMessage('操作成功!', 'success')
// 清空画布并重新获取数据, 首屏刷新
this.opreateFlag = false
this.handleLoad(true, true)
} else {
this.$cwMessage(res.message, 'error')
// 操作执行接口调用结束,放开轮询
this.opreateFlag = false
this.mainLoading = false
}
})
}
})
},
// 处理根据节点状态渲染左键菜单
renderRightMenu(model) {
// 当前状态为等待wait可执行操作为挂起(暂停)pause释放依赖release取消cancel
if (model.state === 'wait') {
return `<p id="pause" class="right-click-menu">挂起</p>
<p id="release" class="right-click-menu">释放依赖</p>
<p id="cancel" class="right-click-menu">取消</p>`
}
// 当前状态为暂停挂起pause可执行操作为恢复resume取消cancel
if (model.state === 'pause') {
return `<p id="resume" class="right-click-menu">恢复</p>
<p id="cancel" class="right-click-menu">取消</p>`
}
// 当前状态为取消cancel可执行的操作为重新执行replay
if (model.state === 'cancel') {
return '<p id="replay" class="right-click-menu">重新执行</p>'
}
// 当前状态为失败fail或错误error或终止stop可执行的操作为取消cancel强制成功success重新执行replay
if (model.state === 'fail' || model.state === 'error' || model.state === 'stop') {
return `<p id="cancel" class="right-click-menu">取消</p>
<p id="success" class="right-click-menu">强制成功</p>
<p id="replay" class="right-click-menu">重新执行</p>`
}
// 当前状态为成功可执行的操作为取消cancel重新执行replay
if (model.state === 'success') {
return `<p id="cancel" class="right-click-menu">取消</p>
<p id="replay" class="right-click-menu">重新执行</p>`
}
// 当前状态为待复核need_confirm可执行的操作为取消cancel复核confirm
if (model.state === 'need_confirm') {
return `<p id="cancel" class="right-click-menu">取消</p>
<p id="confirm" class="right-click-menu">复核</p>`
}
// 当前状态为正在执行run可执行的操作为终止stop
if (model.state === 'run') {
return '<p id="stop" class="right-click-menu">终止</p>'
}
},
handleLoad(clear = false, first = false) {
// 操作进行中,不做轮询
if (this.opreateFlag) {
return false
}
if (first) {
this.formLoading = true
}
// 在操作接口未调用结束的情况下不做轮询
this.$api.nodeRun.retrieve(parseInt(this.$route.query.id)).then(res => {
if (res.result) {
this.form = res.data
if (this.form.hasOwnProperty('log')) {
this.$refs.editorLog.monacoEditor.setValue(this.form.log)
}
if (this.form.hasOwnProperty('script_content')) {
this.$refs.editorScript.monacoEditor.setValue(this.form.script_content)
}
if (this.form.hasOwnProperty('pre_commands')) {
this.$refs.editorPrecommd.monacoEditor.setValue(JSON.stringify(this.form.pre_commands))
this.$refs.editor.monacoEditor.setValue(this.form.pre_commands)
}
// 是否需要清空画布重新渲染
if (clear) {
this.graph.clear()
}
// this.nodeDrawer.show = false
this.renderCanvas(true, first)
const processState = res.data.pipeline_tree.process_state
if (processState === 'success' || processState === 'fail') {
clearInterval(this.timer)
}
} else {
this.$cwMessage(res.message, 'error')
}
this.jobDetailLoading = false
this.formLoading = false
})
}
}
@@ -170,34 +706,92 @@
</script>
<style lang="scss" scoped>
#jobDetail {
padding: 20px;
#jobFlowViewDetail {
padding: 20px;
.box {
.title {
margin-bottom: 12px;
font-size: 14px;
color: #63656E;
font-weight: bold;
height: 22px;
line-height: 22px;
}
.node-drawer {
.customTable {
/deep/ .bk-table-empty-block {
background-color: #fff;
// height: 100%;
/deep/ .custom-sidelider {
.bk-sideslider-wrapper {
.bk-sideslider-content {
height: calc(100% - 60px) !important;
}
}
.custom-textarea {
/deep/ textarea {
padding: 20px;
background-color: rgb(49, 50, 56) !important;
color: #C4C6CC !important;
}
}
margin-bottom: 24px;
}
}
.box {
margin-bottom: 24px;
.title {
margin-bottom: 12px;
font-size: 14px;
color: #63656E;
font-weight: bold;
height: 22px;
line-height: 22px;
}
.customTable {
/deep/ .bk-table-empty-block {
background-color: #fff;
}
}
.custom-textarea {
/deep/ textarea {
padding: 20px;
background-color: rgb(49, 50, 56) !important;
color: #C4C6CC !important;
}
}
#content {
overflow: hidden;
height: 550px;
width: 100%;
position: relative;
background-image: linear-gradient(90deg, rgba(180, 180, 180, 0.15) 10%, rgba(0, 0, 0, 0) 10%), linear-gradient(rgba(180, 180, 180, 0.15) 10%, rgba(0, 0, 0, 0) 10%);
background-size: 10px 10px;
display: flex;
.left-statusList {
height: 100%;
width: 150px;
// padding-left: 20px;
// padding-top: 15px;
}
.right-canvas {
width: 100%;
height: 100%;
position: relative;
#main {
position: relative;
width: 100%;
height: 100%;
/deep/ .right-click-menu:hover {
opacity: .9;
}
/deep/ .right-click-menu {
text-align: center;
color: #fff;
background-color: #3a84ff;
margin-bottom: 8px;
height: 22px;
line-height: 22px;
}
/deep/ .right-click-menu:last-of-type {
margin-bottom: 0;
}
}
}
}
}
}
</style>

View File

@@ -17,6 +17,10 @@
{
name: 'jobflowview',
label: '作业流视图'
},
{
name: 'jobview',
label: '子作业流视图'
}
]
}

View File

@@ -23,19 +23,9 @@
<bk-container :margin="0">
<bk-form :label-width="110">
<bk-row>
<bk-col :span="6">
<bk-form-item label="作业名">
<bk-input :placeholder="'请输入作业名称'" v-model="searchForm.name" clearable></bk-input>
</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="执行者">
<bk-input :placeholder="'请输入执行者'" v-model="searchForm.executor" clearable></bk-input>
</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="作业流名">
<bk-input :placeholder="'请输入作业流名称'" v-model="searchForm.process_run_name" clearable></bk-input>
<bk-input :placeholder="'请输入作业流名称'" v-model="searchForm.name" clearable></bk-input>
</bk-form-item>
</bk-col>
<bk-col :span="6">
@@ -48,43 +38,50 @@
</bk-select>
</bk-form-item>
</bk-col>
</bk-row>
<bk-row style="margin-top: 24px;">
<bk-col :span="6">
<bk-form-item label="Agent">
<bk-input :placeholder="'请输入Agent'" v-model="searchForm.station" clearable></bk-input>
</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="计划开始">
<bk-date-picker :value="searchForm.eta" :placeholder="'选择日期时间'" :type="'datetimerange'"
format="yyyy-MM-dd HH:mm:ss" style="width: 100%;" :transfer="true" @change="handleEtaChange"></bk-date-picker>
</bk-form-item>
</bk-col>
<!-- 未支持 -->
<bk-col :span="6">
<bk-form-item label="实际开始">
<bk-date-picker :value="searchForm.startTime" :placeholder="'选择日期时间'" :type="'datetimerange'"
format="yyyy-MM-dd HH:mm:ss" style="width: 100%;" :transfer="true" @change="handleStartTimeChange"></bk-date-picker>
</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="完成时间">
<bk-date-picker :value="searchForm.endTime" :placeholder="'选择日期时间'" :type="'datetimerange'"
format="yyyy-MM-dd HH:mm:ss" style="width: 100%;" :transfer="true" @change="handleEndTimeChange"></bk-date-picker>
<bk-form-item label="作业总数">
<bk-input :placeholder="'请输入作业总数'" v-model="searchForm.total_job_count" clearable></bk-input>
</bk-form-item>
</bk-col>
</bk-row>
<bk-row style="margin-top: 24px;">
<bk-col :span="6">
<bk-form-item label="已复核人">
<bk-input :placeholder="'请输入已复核人'" v-model="searchForm.confirm_users"></bk-input>
<bk-form-item label="未执行作业数">
<bk-input :placeholder="'请输入未执行作业数'" v-model="searchForm.total_not_execute_job_count"
clearable></bk-input>
</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="待复核人数">
<bk-input :placeholder="'请输入待复核人数'" v-model="searchForm.need_confirm"></bk-input>
<bk-form-item label="释放依赖">
<bk-select class="header-select" :clearable="true" style="background-color: #fff;"
v-model="searchForm.is_release_dependency">
<bk-option v-for="(item, index) in replyList" :key="index" :id="item.value"
:name="item.label">
</bk-option>
</bk-select>
</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="实际开始">
<bk-date-picker :value="searchForm.start_time" :placeholder="'选择日期时间'" :type="'datetimerange'"
format="yyyy-MM-dd HH:mm:ss" style="width: 100%;" :transfer="true" @change="handleStartTimeChange"></bk-date-picker>
</bk-form-item>
</bk-col>
<bk-col :span="6">
<bk-form-item label="完成时间">
<bk-date-picker :value="searchForm.end_time" :placeholder="'选择日期时间'" :type="'datetimerange'"
format="yyyy-MM-dd HH:mm:ss" style="width: 100%;" :transfer="true" @change="handleEndTimeChange"></bk-date-picker>
</bk-form-item>
</bk-col>
</bk-row>
<bk-row style="margin-top: 24px;">
<bk-col :span="6">
<bk-form-item label="跑批系统">
<bk-select :clearable="true" style="background-color: #fff;" v-model="searchForm.category"
@@ -101,6 +98,7 @@
<bk-button style="margin-left: 8px;" @click="handleOpenSeniorSearch">取消</bk-button>
</bk-row>
</bk-form>
</bk-container>
</div>
</div>
@@ -146,13 +144,13 @@
data() {
const fields = [{
id: 'name',
label: '作业名',
label: '作业名',
overflowTooltip: true,
sortable: false
}, {
id: 'process',
label: '所属作业流名',
overflowTooltip: true,
id: 'run_type',
label: '调度方式',
overflowTooltip: false,
sortable: false
}, {
id: 'state',
@@ -160,30 +158,20 @@
overflowTooltip: false,
sortable: false
}, {
id: 'category_name',
label: '跑批系统',
id: 'create_time',
label: '创建时间',
overflowTooltip: false,
sortable: true
}, {
id: 'need_confirm',
label: '待复核人数',
id: 'total_not_execute_job_count',
label: '未执行作业数',
overflowTooltip: false,
sortable: false
}, {
id: 'confirm_users',
label: '已复核人',
overflowTooltip: false,
sortable: false
}, {
id: 'station',
label: 'Agent',
id: 'is_release_dependency',
label: '是否释放依赖',
overflowTooltip: true,
sortable: false
}, {
id: 'executor',
label: '执行者',
overflowTooltip: true,
sortable: false
sortable: true
}, {
id: 'eta',
label: '计划开始时间',
@@ -192,7 +180,7 @@
}, {
id: 'start_time',
label: '实际开始时间',
overflowTooltip: true,
overflowTooltip: false,
sortable: true
}, {
id: 'end_time',
@@ -205,7 +193,7 @@
setting: {
size: 'small', // 表格大小
fields: fields, // 表格所有列
selectedFields: fields // 表格当前显示列
selectedFields: fields.slice(0, 4) // 表格当前显示列
},
opreateFlag: false,
midSearchForm: {},