From 2ce7d5f6fe063c39916a5d0f95a14dab48f4eedc Mon Sep 17 00:00:00 2001 From: waylon <1158341873@qq.com> Date: Tue, 10 Sep 2024 11:25:04 +0800 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20=E5=90=8C=E6=AD=A5=20V1.9.0=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BB=A3=E7=A0=81=20#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bkflow/apigw/docs/apigw-docs.zip | Bin 29298 -> 32174 bytes bkflow/apigw/docs/zh/get_tasks_states.md | 62 +++++ .../apigw/docs/zh/validate_pipeline_tree.md | 260 ++++++++++++++++++ .../commands/data/api-resources.yml | 48 ++++ bkflow/apigw/serializers/task.py | 18 ++ bkflow/apigw/urls.py | 4 + bkflow/apigw/views/get_tasks_states.py | 46 ++++ bkflow/apigw/views/validate_pipeline_tree.py | 42 +++ bkflow/space/configs.py | 4 +- bkflow/utils/mako.py | 28 ++ bkflow/utils/pipeline.py | 4 + config/default.py | 57 ++++ frontend/src/App.vue | 4 +- .../ProcessCanvas/components/dnd/index.vue | 36 +-- .../components/shortcutPanel.vue | 25 +- .../ProcessCanvas/components/tools.vue | 12 +- .../src/components/ProcessCanvas/index.vue | 128 +++++---- .../components/shortcutPanel.vue | 22 +- .../VerticalCanvas/components/tools.vue | 5 +- .../src/components/VerticalCanvas/index.vue | 39 +-- frontend/src/components/layout/Navigation.vue | 3 + .../components/layout/NavigationHeadRight.vue | 6 +- frontend/src/config/i18n/cn.js | 11 +- frontend/src/config/i18n/en.js | 9 +- frontend/src/main.js | 3 + frontend/src/mixins/tplPerspective.js | 1 - frontend/src/store/modules/template.js | 30 +- frontend/src/utils/graphJson.js | 1 + .../ExecuteInfo/JsonschemaForm.vue | 3 - .../task/TaskExecute/TaskOperationHeader.vue | 5 +- .../views/task/TaskExecute/taskCondition.vue | 112 +------- .../template/TemplateEdit/ConditionEdit.vue | 112 ++++---- .../NodeConfig/JsonschemaInputParams.vue | 17 ++ .../NodeConfig/VariablePopover.vue | 3 +- .../template/TemplateEdit/TemplateHeader.vue | 9 - .../src/views/template/TemplateEdit/index.vue | 22 +- .../MockSetting/components/MockConfig.vue | 2 +- .../src/views/template/TemplateMock/index.vue | 1 - requirements.txt | 4 +- tests/engine/utils/test_mako.py | 60 ++++ 40 files changed, 887 insertions(+), 371 deletions(-) create mode 100644 bkflow/apigw/docs/zh/get_tasks_states.md create mode 100644 bkflow/apigw/docs/zh/validate_pipeline_tree.md create mode 100644 bkflow/apigw/views/get_tasks_states.py create mode 100644 bkflow/apigw/views/validate_pipeline_tree.py create mode 100644 bkflow/utils/mako.py create mode 100644 tests/engine/utils/test_mako.py diff --git a/bkflow/apigw/docs/apigw-docs.zip b/bkflow/apigw/docs/apigw-docs.zip index 1e14b43e5ccf382a7261fad3dbb1d515b04dc813..fa4d4e24b1a23a441c287d30ff589fb07cdc3ba1 100644 GIT binary patch delta 2923 zcmai$XHe5y8i)V%5a~pugP>IDMSAanOGJ7LorEHUL@<{Ux`Kd0K%^6-7mt3QC9g zMHqYaTQnJwGN1&{V-=;_5!YssRfsZNNEL!GCJ)A>I4O0MAe%9g*-CsT3JrN02QnM^ zp6aOlMHPB)%L9E#(RRqo;~x<(2?T=I$()66*o=c?^>(+#mHce~RBaCbch!;Mo5eqS zuu&u|cHUvGF&o#Gy;9IEAi|~}ze=JCzttso{MBu#r3*8MJ!*Z~vRL~GYK{|~vkU*R z)%$wZYj1aoxwYnSPkpQJ)Y-G{Lwq|!Mw})oY3=6bTg+^cY;!%Gf+cv-u=txHtLZ~< zkYi#=){Z~`PxZ(aSoziJ3_GV}m5C@bnPao8&M|Ac@+D5v0k>1TJ7pXm^xm}tUQ|#M z8R5A-Z&LX4y>K&pL(TH6HV7A`b`*wxIzj$$nu`v+|Kd7FXuj|D7v!H^%!d=g@F2J% zdopdaeMzA7G`gNy*_KDU{^qXp$eI2>)~Fe?38m=leo&r zr2}(sZWK2Z%J+03mP|gYL3Q=)Uz#qdVev8OSxJ2L5mdB{Kvf+LuQ*;Xf) zgggZ+jYWH!4GvTb(&7rN!|AV+tjj!9GF)1%f7uhu|_H8 z#^VMBIjb?3UTZZsVlle&o~b+`u734Rf8Vom3|O5xmEJIZ{`vC(@F_%^@^$7)2M9|< z8BTRRiB3*P&a}Z{0A^1 z>hg_=bhQ7#b{!fdpL zj9G|QcDkWf_DZqrwbF!O#BIx_Bb`8D`Z7BSA^TaY%M7@^xFctWw(?QrglcsFD9O92 zILqszh5R4+)3Lo8DCf@C6OCq%9WsjE%|&3MY1?;k~4t2BDOnk5~nxHL|6> zzB~M~8r*SM=HQSu+lt|L@|=Al88EJ59Hu}7-qpfM48hUV=Iq(*npa%u?St9O$1A~4 z@rjkfVQDzUruZKS~6Oc|~FH8&H~cjsaut zC&qq9rw70K61OEqD|J{MLgXGZnoxf-o%S?hu-<>6K-p%HShk3dFY{jq8x6@aZ9vv+ z+XMbAR^hH>!7e!V@J0o5-G)b6@!tnH@7NU_x7bvL7J%LWBka3 zR7b(6TVU{Z5z~OCpTGp4`vWz5*Zrx`4%(gXl;ma?yvl4ihr)d}?630B#Y@OY`B3-H3#XdUh+mX$5DhqrQdf$?Ghez$1diTI6*;p%&2ZO9XU%7 zR2RvbCHm8ZV9@Z3_{PIa?yy*}%l>=`L>h9T|oVn8* z)#5VmP(ktticLkLX0sUNu~^2NrQiJP8450FWR1i*a3Y8Js6du1OUZzzoH(4T)uFy7GGM*f*RD2hRbSSf{x|?|+)b+_ zR%Oev z+sn*cy8BF%uA>l}6x^23io-1|XYExCy%=eoy=|>nwfV?$Y&73@k8l)uJZ6nml<`N6 z<$Bwz1s`6Yd%#Hh!1$E;9!>cpYMmNJM8pcZoK5wk4w0n86(bT+#^S!}{Q)2?)S;P} zl|qLz`7EqN6EmC5hP@#+Jg~+OpOvm}wWi@9+rQe2*L6=^*wJmR*?|%pBr01sV^T^+-mGI1J;uJ18tv4ikZij< zpyjo1vN_OqS5lP@g>VNF2S1ngqU73(OkI5swN#o`qH^@a;QOZAhcmgEHa6V8TH{Z| z%wPjG7stKsURm_w6dmYGeZZ*{a*7;Jcb4w2zN?#_RT|eIL1bx+;O!|m}hMJ$(2{$HiXio zlHfY+M|xXdxGb1ilI7#fh65oMqDzMGaW_We20Poke~ircsneLYs#vM6KHOFfct4m{ z`t#)J<4;>=pUcpmb5H}eoqCN6*r*aZPV J1?Kk_{{%XpLks`_ delta 86 zcmZ4YoAJ{VM&1B#W)?065Lng8G?7=5-C*&WG{ePfHtPP% 应用基本设置 -> 基本信息 -> 鉴权信息 获取 | +| bk_app_secret | string | 是 | 安全秘钥(app secret),可以通过 蓝鲸开发者中心 -> 应用基本设置 -> 基本信息 -> 鉴权信息 获取 | + +#### 接口参数 +| 字段 | 类型 | 必选 | 描述 | +|----------|-----------|----|---------------| +| task_ids | list[int] | 是 | 需要查询的任务 id 列表 | + + +### 请求参数示例 +```json +{ + "bk_app_code": "xxxx", + "bk_app_secret": "xxxx", + "bk_username or bk_token": "xxxx", + "task_ids": [1, 2, 3, 4] +} +``` + + +### 返回结果示例 + +```json +{ + "result": true, + "data": { + "3": { + "state": "FINISHED" + }, + "2": { + "state": "FINISHED" + }, + "1": { + "state": "FINISHED" + } + }, + "code": "0", + "message": "" +} +``` +### 返回结果参数说明 + +| 字段 | 类型 | 描述 | +|---------|--------|-----------------------| +| result | bool | 返回结果,true为成功,false为失败 | +| code | int | 返回码,0表示成功,其他值表示失败 | +| message | string | 错误信息 | +| data | dict | 返回数据 | + +#### data[item] + +| 字段 | 类型 | 描述 | +|-----------------|--------|-----------| +| id | string | 任务实例ID | +| state | string | 任务状态 | diff --git a/bkflow/apigw/docs/zh/validate_pipeline_tree.md b/bkflow/apigw/docs/zh/validate_pipeline_tree.md new file mode 100644 index 0000000..edbe61f --- /dev/null +++ b/bkflow/apigw/docs/zh/validate_pipeline_tree.md @@ -0,0 +1,260 @@ +### 资源描述 + +校验任务结构树是否合法 + +### 输入通用参数说明 +| 参数名称 | 参数类型 | 必须 | 参数说明 | +|---------------|--------|----|------------------------------------------------------------| +| bk_app_code | string | 是 | 应用ID(app id),可以通过 蓝鲸开发者中心 -> 应用基本设置 -> 基本信息 -> 鉴权信息 获取 | +| bk_app_secret | string | 是 | 安全秘钥(app secret),可以通过 蓝鲸开发者中心 -> 应用基本设置 -> 基本信息 -> 鉴权信息 获取 | + + +#### 接口参数 + +| 字段 | 类型 | 必选 | 描述 | +|---------------|--------|----|-------| +| pipeline_tree | json | 是 | 任务结构树 | + + + +### 请求参数示例 + +```json +{ + "bk_app_code": "xxxx", + "bk_app_secret": "xxxx", + "bk_username or bk_token": "xxxx", + "pipeline_tree": { + "name": "test", + "activities": { + "nf834705dbbb37c59ad114aa37314975": { + "component": { + "code": "bk_display", + "data": { + "bk_display_message": { + "hook": false, + "need_render": true, + "value": "" + } + }, + "version": "v1.0" + }, + "error_ignorable": false, + "id": "nf834705dbbb37c59ad114aa37314975", + "incoming": [ + "lee4ca362c673536958aa656cb36efda" + ], + "loop": null, + "name": "消息展示", + "optional": true, + "outgoing": "l2e819009a9a3714ab108ad5c594bb73", + "stage_name": "", + "type": "ServiceActivity", + "retryable": true, + "skippable": true, + "auto_retry": { + "enable": false, + "interval": 0, + "times": 1 + }, + "timeout_config": { + "enable": false, + "seconds": 10, + "action": "forced_fail" + }, + "labels": [] + } + }, + "end_event": { + "id": "n2dfd1233f633cdf864fc3681eb2b0b3", + "incoming": [ + "l2e819009a9a3714ab108ad5c594bb73" + ], + "name": "", + "outgoing": "", + "type": "EmptyEndEvent", + "labels": [] + }, + "flows": { + "lee4ca362c673536958aa656cb36efda": { + "id": "lee4ca362c673536958aa656cb36efda", + "is_default": false, + "source": "n1df1598dba137aba81094851379435a", + "target": "nf834705dbbb37c59ad114aa37314975" + }, + "l2e819009a9a3714ab108ad5c594bb73": { + "id": "l2e819009a9a3714ab108ad5c594bb73", + "is_default": false, + "source": "nf834705dbbb37c59ad114aa37314975", + "target": "n2dfd1233f633cdf864fc3681eb2b0b3" + } + }, + "gateways": {}, + "line": [ + { + "id": "lee4ca362c673536958aa656cb36efda", + "source": { + "arrow": "Right", + "id": "n1df1598dba137aba81094851379435a" + }, + "target": { + "arrow": "Left", + "id": "nf834705dbbb37c59ad114aa37314975" + } + }, + { + "id": "l2e819009a9a3714ab108ad5c594bb73", + "source": { + "arrow": "Right", + "id": "nf834705dbbb37c59ad114aa37314975" + }, + "target": { + "arrow": "Left", + "id": "n2dfd1233f633cdf864fc3681eb2b0b3" + } + } + ], + "location": [ + { + "id": "n1df1598dba137aba81094851379435a", + "type": "startpoint", + "x": 40, + "y": 150 + }, + { + "id": "nf834705dbbb37c59ad114aa37314975", + "type": "tasknode", + "name": "消息展示", + "stage_name": "", + "x": 240, + "y": 140, + "group": "蓝鲸服务(BK)", + "icon": "", + "optional": true, + "error_ignorable": false, + "retryable": true, + "skippable": true, + "auto_retry": { + "enable": false, + "interval": 0, + "times": 1 + }, + "timeout_config": { + "enable": false, + "seconds": 10, + "action": "forced_fail" + } + }, + { + "id": "n2dfd1233f633cdf864fc3681eb2b0b3", + "type": "endpoint", + "x": 540, + "y": 150 + } + ], + "outputs": [], + "start_event": { + "id": "n1df1598dba137aba81094851379435a", + "incoming": "", + "name": "", + "outgoing": "lee4ca362c673536958aa656cb36efda", + "type": "EmptyStartEvent", + "labels": [] + }, + "constants": {}, + "projectBaseInfo": {}, + "notify_receivers": { + "receiver_group": [], + "more_receiver": "" + }, + "notify_type": { + "success": [], + "fail": [] + }, + "template_labels": [], + "internalVariable": { + "${_system.task_name}": { + "key": "${_system.task_name}", + "name": "任务名称", + "index": -1, + "desc": "", + "show_type": "hide", + "source_type": "system", + "source_tag": "", + "source_info": {}, + "custom_type": "", + "value": "", + "hook": false, + "validation": "" + }, + "${_system.task_id}": { + "key": "${_system.task_id}", + "index": -2, + "name": "任务ID", + "desc": "", + "show_type": "hide", + "source_type": "system", + "source_tag": "", + "source_info": {}, + "custom_type": "", + "value": "", + "hook": false, + "validation": "" + }, + "${_system.task_start_time}": { + "key": "${_system.task_start_time}", + "name": "任务开始时间", + "index": -3, + "desc": "", + "show_type": "hide", + "source_type": "system", + "source_tag": "", + "source_info": {}, + "custom_type": "", + "value": "", + "hook": false, + "validation": "" + }, + "${_system.operator}": { + "key": "${_system.operator}", + "name": "任务的执行人(点击开始执行的人员)", + "index": -4, + "desc": "", + "show_type": "hide", + "source_type": "system", + "source_tag": "", + "source_info": {}, + "custom_type": "", + "value": "", + "hook": false, + "validation": "" + } + }, + "spaceId": 1, + "scopeInfo": { + "scope_type": null, + "scope_value": null + } + } +} +``` + +### 返回结果示例 + +```json +{ + "result": true, + "data": {}, + "code": "0", + "message": "" +} + +``` +### 返回结果参数说明 + +| 字段 | 类型 | 描述 | +|---------|--------|-----------------------| +| result | bool | 返回结果,true为成功,false为失败 | +| code | int | 返回码,0表示成功,其他值表示失败 | +| message | string | 错误信息 | +| data | dict | 返回数据 | diff --git a/bkflow/apigw/management/commands/data/api-resources.yml b/bkflow/apigw/management/commands/data/api-resources.yml index 26d9a83..29e447c 100644 --- a/bkflow/apigw/management/commands/data/api-resources.yml +++ b/bkflow/apigw/management/commands/data/api-resources.yml @@ -127,6 +127,30 @@ paths: userVerifiedRequired: false disabledStages: [ ] descriptionEn: Create a task without template + /space/{space_id}/validate_pipeline_tree/: + post: + operationId: validate_pipeline_tree + description: 校验 pipeline_tree 是否合法 + tags: [ ] + responses: + default: + description: '' + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + type: HTTP + method: post + path: /{env.api_sub_path}apigw/space/{space_id}/validate_pipeline_tree/ + matchSubpath: false + timeout: 0 + upstreams: { } + transformHeaders: { } + authConfig: + userVerifiedRequired: false + disabledStages: [ ] + descriptionEn: Validate a pipeline tree /space/{space_id}/create_mock_task/: post: operationId: create_mock_task @@ -319,6 +343,30 @@ paths: userVerifiedRequired: false disabledStages: [] descriptionEn: Get states of a task + /space/{space_id}/get_tasks_states/: + post: + operationId: get_tasks_states + description: 批量获取任务状态 + tags: [ ] + responses: + default: + description: '' + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: true + matchSubpath: false + backend: + type: HTTP + method: post + path: /{env.api_sub_path}apigw/space/{space_id}/get_tasks_states/ + matchSubpath: false + timeout: 0 + upstreams: { } + transformHeaders: { } + authConfig: + userVerifiedRequired: false + disabledStages: [ ] + descriptionEn: Get states of a task with filter params /space/{space_id}/task/{task_id}/operate_task/{operation}/: post: operationId: operate_task diff --git a/bkflow/apigw/serializers/task.py b/bkflow/apigw/serializers/task.py index 5c17f1b..db1bdd1 100644 --- a/bkflow/apigw/serializers/task.py +++ b/bkflow/apigw/serializers/task.py @@ -18,9 +18,12 @@ to the current version of the project delivered to anyone in the future. """ from django.utils.translation import ugettext_lazy as _ +from pipeline.exceptions import PipelineException from rest_framework import serializers from bkflow.constants import MAX_LEN_OF_TASK_NAME, USER_NAME_MAX_LENGTH +from bkflow.pipeline_web.parser.validator import validate_web_pipeline_tree +from bkflow.utils.strings import standardize_pipeline_node_name class CreateTaskSerializer(serializers.Serializer): @@ -66,6 +69,17 @@ class CreateTaskWithoutTemplateSerializer(serializers.Serializer): pipeline_tree = serializers.JSONField(help_text=_("任务树"), required=True) +class PipelineTreeSerializer(serializers.Serializer): + pipeline_tree = serializers.JSONField(help_text=_("任务树"), required=True) + + def validate_pipeline_tree(self, pipeline_tree): + try: + standardize_pipeline_node_name(pipeline_tree) + validate_web_pipeline_tree(pipeline_tree) + except PipelineException as e: + raise serializers.ValidationError(str(e)) + + class GetTaskListSerializer(serializers.Serializer): scope_type = serializers.CharField(help_text=_("流程范围类型"), max_length=128, required=False) scope_value = serializers.CharField(help_text=_("流程范围值"), max_length=128, required=False) @@ -77,6 +91,10 @@ class GetTaskListSerializer(serializers.Serializer): name = serializers.CharField(help_text=_("任务名"), max_length=MAX_LEN_OF_TASK_NAME, required=False) +class GetTasksStatesSerializer(serializers.Serializer): + task_ids = serializers.ListField(required=True, child=serializers.IntegerField()) + + class OperateTaskSerializer(serializers.Serializer): operator = serializers.CharField(help_text=_("操作人"), max_length=USER_NAME_MAX_LENGTH, required=True) diff --git a/bkflow/apigw/urls.py b/bkflow/apigw/urls.py index 9931159..8157a1b 100644 --- a/bkflow/apigw/urls.py +++ b/bkflow/apigw/urls.py @@ -41,6 +41,7 @@ from bkflow.apigw.views.get_task_list import get_task_list from bkflow.apigw.views.get_task_node_detail import get_task_node_detail from bkflow.apigw.views.get_task_states import get_task_states + from bkflow.apigw.views.get_tasks_states import get_tasks_states from bkflow.apigw.views.get_template_detail import get_template_detail from bkflow.apigw.views.get_template_list import get_template_list from bkflow.apigw.views.grant_apigw_permissions_to_app import ( @@ -51,6 +52,7 @@ from bkflow.apigw.views.renew_space_config import renew_space_config from bkflow.apigw.views.revoke_token import revoke_token from bkflow.apigw.views.update_template import update_template + from bkflow.apigw.views.validate_pipeline_tree import validate_pipeline_tree urlpatterns += [ url(r"^create_space/$", create_space), @@ -67,10 +69,12 @@ url(r"^space/(?P\d+)/create_task/$", create_task), url(r"^space/(?P\d+)/create_mock_task/$", create_mock_task), url(r"^space/(?P\d+)/create_task_without_template/$", create_task_without_template), + url(r"^space/(?P\d+)/validate_pipeline_tree/$", validate_pipeline_tree), url(r"^space/(?P\d+)/create_credential/$", create_credential), url(r"^space/(?P\d+)/get_task_list/$", get_task_list), url(r"^space/(?P\d+)/task/(?P\d+)/get_task_detail/$", get_task_detail), url(r"^space/(?P\d+)/task/(?P\d+)/get_task_states/$", get_task_states), + url(r"^space/(?P\d+)/get_tasks_states/$", get_tasks_states), url( r"^space/(?P\d+)/task/(?P\d+)/node/(?P\w+)/get_task_node_detail/$", get_task_node_detail, diff --git a/bkflow/apigw/views/get_tasks_states.py b/bkflow/apigw/views/get_tasks_states.py new file mode 100644 index 0000000..320e74d --- /dev/null +++ b/bkflow/apigw/views/get_tasks_states.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making +蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available. +Copyright (C) 2024 THL A29 Limited, +a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +We undertake not to change the open source license (MIT license) applicable + +to the current version of the project delivered to anyone in the future. +""" +import json + +from apigw_manager.apigw.decorators import apigw_require +from blueapps.account.decorators import login_exempt +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST + +from bkflow.apigw.decorators import check_jwt_and_space, return_json_response +from bkflow.apigw.serializers.task import GetTasksStatesSerializer +from bkflow.contrib.api.collections.task import TaskComponentClient + + +@login_exempt +@csrf_exempt +@require_POST +@apigw_require +@check_jwt_and_space +@return_json_response +def get_tasks_states(request, space_id): + data = json.loads(request.body) + ser = GetTasksStatesSerializer(data=data) + ser.is_valid(raise_exception=True) + + client = TaskComponentClient(space_id=space_id) + data = {"space_id": space_id, **ser.validated_data} + result = client.get_tasks_states(data=data) + return result diff --git a/bkflow/apigw/views/validate_pipeline_tree.py b/bkflow/apigw/views/validate_pipeline_tree.py new file mode 100644 index 0000000..dabb8f5 --- /dev/null +++ b/bkflow/apigw/views/validate_pipeline_tree.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making +蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available. +Copyright (C) 2024 THL A29 Limited, +a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +We undertake not to change the open source license (MIT license) applicable + +to the current version of the project delivered to anyone in the future. +""" +import json + +from apigw_manager.apigw.decorators import apigw_require +from blueapps.account.decorators import login_exempt +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST + +from bkflow.apigw.decorators import check_jwt_and_space, return_json_response +from bkflow.apigw.serializers.task import PipelineTreeSerializer +from bkflow.utils import err_code + + +@login_exempt +@csrf_exempt +@require_POST +@apigw_require +@check_jwt_and_space +@return_json_response +def validate_pipeline_tree(request, space_id): + data = json.loads(request.body) + ser = PipelineTreeSerializer(data=data) + ser.is_valid(raise_exception=True) + return {"result": True, "data": {}, "message": "", "code": err_code.SUCCESS.code} diff --git a/bkflow/space/configs.py b/bkflow/space/configs.py index c325a97..c55973a 100644 --- a/bkflow/space/configs.py +++ b/bkflow/space/configs.py @@ -271,14 +271,14 @@ class GatewayExpressionConfig(BaseSpaceConfig): name = "gateway_expression" desc = _("网关表达式") default_value = "boolrule" - choices = ["boolrule", "FEEL"] + choices = ["boolrule", "FEEL", "MAKO"] @classmethod def validate(cls, value: str): if value not in cls.choices: raise ValidationError( f"[validate gateway expression error]: gateway expression only support " - f"'boolrule' or 'FEEL', value: {value}" + f"'boolrule' or 'FEEL' or 'MAKO', value: {value}" ) return True diff --git a/bkflow/utils/mako.py b/bkflow/utils/mako.py new file mode 100644 index 0000000..c2169d8 --- /dev/null +++ b/bkflow/utils/mako.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making +蓝鲸流程引擎服务 (BlueKing Flow Engine Service) available. +Copyright (C) 2024 THL A29 Limited, +a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +We undertake not to change the open source license (MIT license) applicable + +to the current version of the project delivered to anyone in the future. +""" + +from bamboo_engine.template.template import Template + + +def parse_mako_expression(expression, context): + parsed_result = Template(expression).render(context) + if parsed_result not in ["True", "False"]: + raise ValueError(f"The result of the expression must be a boolean, now is {parsed_result}") + return parsed_result == "True" diff --git a/bkflow/utils/pipeline.py b/bkflow/utils/pipeline.py index a14f985..382315a 100644 --- a/bkflow/utils/pipeline.py +++ b/bkflow/utils/pipeline.py @@ -23,6 +23,8 @@ from bkflow_feel.api import parse_expression from pipeline.parser.utils import recursive_replace_id +from bkflow.utils.mako import parse_mako_expression + DEFAULT_HORIZONTAL_PIPELINE_TREE = { "activities": { "node89f4f55f853d71c6a15e83d0d0ca": { @@ -208,4 +210,6 @@ def build_default_pipeline_tree(horizontal_canvas=True): def pipeline_gateway_expr_func(expr: str, context: dict, extra_info: dict, *args, **kwargs) -> bool: if extra_info.get("parse_lang") == "FEEL": return parse_expression(expression=expr) + if extra_info.get("parse_lang") == "MAKO": + return parse_mako_expression(expression=expr, context=context) return BoolRule(expr).test() diff --git a/config/default.py b/config/default.py index 47e4dcc..094d687 100644 --- a/config/default.py +++ b/config/default.py @@ -22,6 +22,7 @@ import datetime import json +from bamboo_engine.config import Settings as BambooSettings from blueapps.conf.default_settings import * # noqa from blueapps.conf.log import get_logging_config_dict from blueapps.opentelemetry.utils import inject_logging_trace_info @@ -128,6 +129,62 @@ UUID_DIGIT_STARTS_SENSITIVE = True PIPELINE_EXCLUSIVE_GATEWAY_EXPR_FUNC = pipeline_gateway_expr_func +# pipeline mako render settings +MAKO_SANDBOX_SHIELD_WORDS = [ + "ascii", + "bytearray", + "bytes", + "callable", + "chr", + "classmethod", + "compile", + "delattr", + "dir", + "divmod", + "exec", + "eval", + "filter", + "frozenset", + "getattr", + "globals", + "hasattr", + "hash", + "help", + "id", + "input", + "isinstance", + "issubclass", + "iter", + "locals", + "map", + "memoryview", + "next", + "object", + "open", + "print", + "property", + "repr", + "setattr", + "staticmethod", + "super", + "type", + "vars", + "__import__", +] +BambooSettings.MAKO_SANDBOX_SHIELD_WORDS = MAKO_SANDBOX_SHIELD_WORDS +MAKO_SANDBOX_IMPORT_MODULES = { + "datetime": "datetime", + "re": "re", + "hashlib": "hashlib", + "random": "random", + "time": "time", + "os.path": "os.path", + "json": "json", +} +BambooSettings.MAKO_SANDBOX_IMPORT_MODULES = MAKO_SANDBOX_IMPORT_MODULES +# 支持 mako 表达式在 dict/list/tuple 情况下嵌套索引 +BambooSettings.ENABLE_RENDER_OBJ_BY_MAKO_STRING = True + # 所有环境的日志级别可以在这里配置 # LOG_LEVEL = 'INFO' diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 35f673b..37cbf87 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -191,8 +191,8 @@ body { min-width: 1366px; } .with-system-notice { - height: calc(100vh - 40px); - /deep/.container-content { + height: calc(100vh - 40px) !important; + .container-content { max-height: calc(100vh - 92px)!important; } } diff --git a/frontend/src/components/ProcessCanvas/components/dnd/index.vue b/frontend/src/components/ProcessCanvas/components/dnd/index.vue index 0dcc026..373cdf3 100644 --- a/frontend/src/components/ProcessCanvas/components/dnd/index.vue +++ b/frontend/src/components/ProcessCanvas/components/dnd/index.vue @@ -10,25 +10,10 @@ 'node-item', node.id === 'group' ? 'group-node' : `common-icon-node-${node.icon}`, { disabled: ['start', 'end'].includes(node.id) }, - { actived: node.id === 'task' && menuType === 'plugin' } ]" - :data-type="node.id" - @click="menuType = node.id === 'task' ? 'plugin' : ''"> - group + :data-type="node.id"> - - - diff --git a/frontend/src/views/template/TemplateEdit/NodeConfig/VariablePopover.vue b/frontend/src/views/template/TemplateEdit/NodeConfig/VariablePopover.vue index ef8746e..0be27a6 100644 --- a/frontend/src/views/template/TemplateEdit/NodeConfig/VariablePopover.vue +++ b/frontend/src/views/template/TemplateEdit/NodeConfig/VariablePopover.vue @@ -77,12 +77,13 @@
+ }" + class="user-avatar"> {{ username }} - +
、>=、<、<=" 等二元比较操作符': '支持 "=、!=、>、>=、<、<=" 等二元比较操作符', + feelOperates: '支持 "=、!=、>、>=、<、<=" 等二元比较操作符', '支持 "and、or、true、false" 等关键字语法': '支持 "and、or、true、false" 等关键字语法', '支持 FEEL (Friendly Enough Expression Language) 基础语法': '支持 FEEL (Friendly Enough Expression Language) 基础语法', '支持使用全局变量,如': '支持使用全局变量,如', '${}中支持使用内置函数、datetime、re、hashlib、random、time、os.path模块处理全局变量': '${}中支持使用内置函数、datetime、re、hashlib、random、time、os.path模块处理全局变量', - "因 MAKO 语法限制:请勿使用 '$', '{' 和 '}' 字符": "因 MAKO 语法限制:请勿使用 '$', '{' 和 '}' 字符", + '因 MAKO 语法限制:请勿使用 \'$\', \'{\' 和 \'}\' 字符': '因 MAKO 语法限制:请勿使用 \'$\', \'{\' 和 \'}\' 字符', '示例:': '示例:', '字符串比较:': '字符串比较:', '数值比较:': '数值比较:', '包含:': '包含:', - '支持 "==、!=、>、>=、<、<=、in、notin" 等二元比较操作符': '支持 "==、!=、>、>=、<、<=、in、notin" 等二元比较操作符', + makoOperates: '支持 "==、!=、>、>=、<、<=、in、notin" 等二元比较操作符', '支持 "and、or、True/true、False/false" 等关键字语法': '支持 "and、or、True/true、False/false" 等关键字语法', 表达式更多细节请参考: '表达式更多细节请参考', '支持使用内置函数、datetime、re、hashlib、random、time、os.path模块处理全局变量': '支持使用内置函数、datetime、re、hashlib、random、time、os.path模块处理全局变量', '假设 key 是值为 "3" 的全局变量': '假设 key 是值为 "3" 的全局变量', '使用内置函数、datetime、re、hashlib、random、time、os.path模块处理全局变量': '使用内置函数、datetime、re、hashlib、random、time、os.path模块处理全局变量', - '支持直接引用全局变量': '支持直接引用全局变量', + 支持直接引用全局变量: '支持直接引用全局变量', '支持 "and、or、not、True、False" 等 MAKO 关键字语法': '支持 "and、or、not、True、False" 等 MAKO 关键字语法', '所有分支均不匹配时执行,类似switch-case-default里面的default': '所有分支均不匹配时执行,类似switch-case-default里面的default', 任务执行信息: '任务执行信息', diff --git a/frontend/src/config/i18n/en.js b/frontend/src/config/i18n/en.js index e97cfa5..f5fc7b0 100644 --- a/frontend/src/config/i18n/en.js +++ b/frontend/src/config/i18n/en.js @@ -530,24 +530,24 @@ const en = { 分支类型: 'Branch Type', 自定义分支: 'Custom Branch', 默认分支: 'Default Branch', - '支持 "=、!=、>、>=、<、<=" 等二元比较操作符': 'Supports binary comparison operators such as =, !=, >, >=, <, <=', + feelOperates: 'Supports binary comparison operators such as =, !=, >, >=, <, <=', '支持 "and、or、true、false" 等关键字语法': 'Supports keywords such as and, or, true, false', '支持 FEEL (Friendly Enough Expression Language) 基础语法': 'Supports basic FEEL (Friendly Enough Expression Language) syntax', '支持使用全局变量,如': 'Supports using global variables, such as', '${}中支持使用内置函数、datetime、re、hashlib、random、time、os.path模块处理全局变量': 'support using built-in functions, datetime, re, hashlib, random, time, os in ${}', - "因 MAKO 语法限制:请勿使用 '$', '{' 和 '}' 字符": "Due to MAKO syntax limitations: please do not use the characters '$', '{', and '}'", + '因 MAKO 语法限制:请勿使用 \'$\', \'{\' 和 \'}\' 字符': 'Due to MAKO syntax limitations: please do not use the characters \'$\', \'{\', and \'}\'', '示例:': 'Example:', '字符串比较:': 'String Comparison:', '数值比较:': 'Numerical Comparison:', '包含:': 'Contains:', - '支持 "==、!=、>、>=、<、<=、in、notin" 等二元比较操作符': 'Supports binary comparison operators such as ==, !=, >, >=, <, <=, in, notin', + makoOperates: 'Supports binary comparison operators such as ==, !=, >, >=, <, <=, in, notin', '支持 "and、or、True/true、False/false" 等关键字语法': 'Supports keywords such as and, or, True/true, False/false', 表达式更多细节请参考: 'For more details on expressions, please refer to', '支持使用内置函数、datetime、re、hashlib、random、time、os.path模块处理全局变量': 'support using built-in functions, datetime, re, hashlib, random, time, os', - '假设 key 是值为 "3" 的全局变量': "Assume key is a global variable with a value of '3'", + '假设 key 是值为 "3" 的全局变量': 'Assume key is a global variable with a value of \'3\'', '使用内置函数、datetime、re、hashlib、random、time、os.path模块处理全局变量': 'Support using built-in functions, datetime, re, hashlib, random, time, os', '支持 "and、or、not、True、False" 等 MAKO 关键字语法': 'Supports MAKO keywords such as and, or, not, True, False', - '支持直接引用全局变量': 'Support Direct Reference to Global Variables', + 支持直接引用全局变量: 'Support Direct Reference to Global Variables', '所有分支均不匹配时执行,类似switch-case-default里面的default': 'Executed when no branches match, similar to default in switch-case-default', 任务执行信息: 'Task Execution Information', '存在子流程节点执行失败,可从节点执行记录去往子任务处理,并及时': 'Subprocess node execution failed, can go to the subtask from the node execution records and promptly', diff --git a/frontend/src/main.js b/frontend/src/main.js index 799993a..b6a85a1 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -215,7 +215,7 @@ Validator.localize({ regex: i18n.t('请输入 test 之间的数'), }, expression: { - regex: i18n.t("因 MAKO 语法限制:请勿使用 '$', '{' 和 '}' 字符"), + regex: i18n.t('因 MAKO 语法限制:请勿使用 \'$\', \'{\' 和 \'}\' 字符'), }, }, }, diff --git a/frontend/src/store/modules/template.js b/frontend/src/store/modules/template.js index da2c3fd..c7fb796 100644 --- a/frontend/src/store/modules/template.js +++ b/frontend/src/store/modules/template.js @@ -589,7 +589,7 @@ const template = { evaluate = Object.keys(conditions).length ? '1 == 0' : '1 == 1'; } if (line.parseLang === 'MAKO') { - evaluate = `\${${evaluate}}` + evaluate = `\${${evaluate}}`; } const defaultName = i18n.t('条件'); const regStr = `^${i18n.t('条件')}[0-9]*$`; @@ -803,9 +803,12 @@ const template = { if (location.type === 'branchgateway' || location.type === 'conditionalparallelgateway') { state.gateways[location.id].conditions = {}; // 网关分支表达式解析类型字段 - let { parseLang } = location - const oldGatewayInfo = state.gateways[location.oldSouceId] - parseLang = oldGatewayInfo ? oldGatewayInfo.extra_info?.parse_lang : parseLang + let { parseLang } = location; + const oldGatewayInfo = state.gateways[location.oldSouceId]; + if (oldGatewayInfo) { + const { parse_lang: oldParseLang } = oldGatewayInfo.extra_info || {}; + parseLang = oldParseLang; + } if (['FEEL', 'MAKO'].includes(parseLang)) { state.gateways[location.id].extra_info = { parse_lang: parseLang }; } diff --git a/frontend/src/views/task/TaskExecute/TaskOperationHeader.vue b/frontend/src/views/task/TaskExecute/TaskOperationHeader.vue index e206072..f2a202c 100644 --- a/frontend/src/views/task/TaskExecute/TaskOperationHeader.vue +++ b/frontend/src/views/task/TaskExecute/TaskOperationHeader.vue @@ -37,14 +37,13 @@
- + :to="`/template/view/${templateId}/`" /> {{ stateStr }} diff --git a/frontend/src/views/task/TaskExecute/taskCondition.vue b/frontend/src/views/task/TaskExecute/taskCondition.vue index 3a5092a..3503b5f 100644 --- a/frontend/src/views/task/TaskExecute/taskCondition.vue +++ b/frontend/src/views/task/TaskExecute/taskCondition.vue @@ -41,8 +41,7 @@
- + :parse-lang="gwConfig.extra_info && gwConfig.extra_info.parse_lang" />
- + :parse-lang="gwConfig.extra_info && gwConfig.extra_info.parse_lang" /> { - const component = properties[form]['ui:component'] - if (!component) { - properties[form]['ui:component'] = { - props: { disabled: true } - } - } else if ('props' in component) { - component.props.disabled = true - } else { - component.props = { disabled: true } - } - if (properties[form].items?.properties) { - this.setDisabledProps(properties[form].items?.properties) - } - }) - }, }, }; diff --git a/frontend/src/views/template/TemplateEdit/components/ConditionExpression.vue b/frontend/src/views/template/TemplateEdit/components/ConditionExpression.vue new file mode 100644 index 0000000..6c11bd2 --- /dev/null +++ b/frontend/src/views/template/TemplateEdit/components/ConditionExpression.vue @@ -0,0 +1,83 @@ + + + diff --git a/frontend/src/views/template/TemplateEdit/index.vue b/frontend/src/views/template/TemplateEdit/index.vue index c41a3ee..fa7c856 100644 --- a/frontend/src/views/template/TemplateEdit/index.vue +++ b/frontend/src/views/template/TemplateEdit/index.vue @@ -1562,9 +1562,11 @@ line.oldSouceId = data.oldSouceId; } // 添加语法标识 - const gatewayInfo = this.gateways[line.source.id] + line.parseLang = this.spaceRelatedConfig.gateway_expression; + const gatewayInfo = this.gateways[line.source.id]; if (gatewayInfo) { - line.parseLang = gatewayInfo ? gatewayInfo.extra_info?.parse_lang : this.spaceRelatedConfig.gateway_expression; + const { parse_lang: parseLang } = gatewayInfo.extra_info || {}; + line.parseLang = parseLang; } this.setLine({ type, line }); // 对校验失败节点进行处理 diff --git a/frontend/src/views/template/TemplateMock/MockSetting/components/MockConfig.vue b/frontend/src/views/template/TemplateMock/MockSetting/components/MockConfig.vue index 46009b4..21d9233 100644 --- a/frontend/src/views/template/TemplateMock/MockSetting/components/MockConfig.vue +++ b/frontend/src/views/template/TemplateMock/MockSetting/components/MockConfig.vue @@ -33,9 +33,9 @@ @click.stop> Date: Thu, 12 Sep 2024 11:54:47 +0800 Subject: [PATCH 03/20] =?UTF-8?q?fix:=20=E5=86=B3=E7=AD=96=E8=A1=A8?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AF=BC=E5=85=A5=E5=AF=BC=E5=87=BA=20--stor?= =?UTF-8?q?y=3D119248396=20#=20Reviewed,=20transaction=20id:=2018227?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 1 + .../DecisionTable/ImportExport/ExportBtn.vue | 170 ++++++++++++++++ .../DecisionTable/ImportExport/ImportBtn.vue | 191 ++++++++++++++++++ .../ImportExport/dataTransfer.js | 158 +++++++++++++++ .../DecisionTable/components/TableHeader.vue | 38 +++- .../src/components/DecisionTable/index.vue | 5 + .../DecisionTable/components/DecisionEdit.vue | 7 - 7 files changed, 562 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/DecisionTable/ImportExport/ExportBtn.vue create mode 100644 frontend/src/components/DecisionTable/ImportExport/ImportBtn.vue create mode 100644 frontend/src/components/DecisionTable/ImportExport/dataTransfer.js diff --git a/frontend/package.json b/frontend/package.json index e403d78..64e3b2a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -48,6 +48,7 @@ "vuedraggable": "^2.24.3", "vuex": "^2.4.0", "xlsx": "^0.18.5", + "xlsx-js-style": "^1.2.0", "xss": "^1.0.15" }, "devDependencies": { diff --git a/frontend/src/components/DecisionTable/ImportExport/ExportBtn.vue b/frontend/src/components/DecisionTable/ImportExport/ExportBtn.vue new file mode 100644 index 0000000..9967c26 --- /dev/null +++ b/frontend/src/components/DecisionTable/ImportExport/ExportBtn.vue @@ -0,0 +1,170 @@ + + + diff --git a/frontend/src/components/DecisionTable/ImportExport/ImportBtn.vue b/frontend/src/components/DecisionTable/ImportExport/ImportBtn.vue new file mode 100644 index 0000000..4e0496b --- /dev/null +++ b/frontend/src/components/DecisionTable/ImportExport/ImportBtn.vue @@ -0,0 +1,191 @@ + + + diff --git a/frontend/src/components/DecisionTable/ImportExport/dataTransfer.js b/frontend/src/components/DecisionTable/ImportExport/dataTransfer.js new file mode 100644 index 0000000..7f5a75d --- /dev/null +++ b/frontend/src/components/DecisionTable/ImportExport/dataTransfer.js @@ -0,0 +1,158 @@ +const operateMap = { + equals: '', + 'not-equals': '!=', + contains: 'contains', + 'not-contains': 'not contains', + 'greater-than': '>', + 'less-than': '<', + 'greater-than-or-equal': '>=', + 'less-than-or-equal': '<=', + // 范围条件特殊处理 +}; + +const intRegex = /^\d+$/; + +export const getCellText = (data = {}) => { + const { compare, right = {} } = data; + if (!compare) return '--'; + const { type, value } = right.obj; + // 非范围条件 + if (compare in operateMap) { + return `${operateMap[compare]} ${value}`; + } + // 范围条件 + const isIntRange = type === 'int[Range]'; + let text = Array.isArray(value) ? value.join(',') : value; + text = isIntRange ? (`${value.start},${value.end}`) : text; + return `${compare === 'in-range' ? '' : '!'}[${text}]`; +}; + +export const validateFiled = (data = []) => { + const keys = ['desc', 'from', 'id', 'name', 'tips', 'type']; + const requiredKeys = ['from', 'id', 'name', 'type']; + let message = ''; + const ids = data.map(field => field.id); + + data.every((field) => { + // 不存在类型 + if (!['string', 'int', 'select'].includes(field.type)) { + message = `【${field.name}】列注释配置中type类型不存在`; + return false; + } + if (!['inputs', 'outputs'].includes(field.from)) { + message = `【${field.name}】列注释配置中from类型不存在`; + return false; + } + + // 下拉框类型 + if (field.type === 'select') { + keys.push('options'); + } + + // 缺少字段 + const missingKeys = keys.filter(key => !(key in field)); + if (missingKeys.length) { + message = `【${field.name}】列注释配置中缺少${missingKeys.join(',')}字段`; + return false; + } + + // id校验 + const idPattern = /^[a-zA-Z][a-zA-Z0-9]*$/.test(field.id); + if (!idPattern) { + message = `【${field.name}】列注释配置中key值只能由数字和字母组成且不能以数字开头`; + return false; + } + const idCount = ids.filter(id => id === field.id).length; + if (idCount > 1) { + message = `【${field.name}】列注释配置中字段标识已经存在`; + return false; + } + + // 必填字段没值 + const emptyRequiredKeys = requiredKeys.filter(key => !field[key]); + if (emptyRequiredKeys.length) { + message = `【${field.name}】列注释配置中缺少${emptyRequiredKeys.join(',')}字段为必填项`; + return false; + } + + // 下拉框类型 + if (field.type === 'select') { + // 校验下拉框类型格式 + const { options = {} } = field; + let isOptionsValid = Object.prototype.toString.call(options) === '[object Object]'; + isOptionsValid = isOptionsValid && (Array.isArray(options.items) && options.type === 'custom'); + isOptionsValid = isOptionsValid && options.items.every(item => (item.id && item.name)); + if (!isOptionsValid) { + message = `【${field.name}】列注释配置中options结构不对`; + return false; + } + } + + return true; + }); + + return message; +}; + +export const parseValue = (data = '') => { + // 解析出表格实际值,类型 + let value = String(data).trim(); + let type = 'equals'; + + Object.keys(operateMap).forEach((key) => { + const text = operateMap[key]; + if (text) { + const regex = new RegExp(`^${text}`); + if (regex.test(value)) { + [, value] = value.split(text); + type = key; + } + return; + } + // 数字 + value = intRegex.test(value) ? Number(value) : value; + // 范围条件 + if (value && /^(!)?\[.*\]$/.test(value)) { + const [v1, v2] = value.match(/^(!)?\[.*\]$/); + value = v1.slice(v2 ? 2 : 1, -1).split(',') + .map(item => intRegex.test(item) ? Number(item) : item.trim()); + type = `${v2 ? 'not-' : ''}in-range`; + } + }); + + return { value, type }; +}; + +export const validateValue = (value, config) => { + let message = ''; + + // 判断数据类型是否合规 + if (config.type === 'int') { + const isMatch = Array.isArray(value) ? value.every(item => intRegex.test(item)) : intRegex.test(value); + if (!isMatch) { + message = `【${config.name}】列存在非数字类型`; + } + } else if (config.type === 'select' && value) { + const ids = Array.isArray(value) ? value : [value]; + const isMatch = ids.every(id => config.options.items.some(item => item.id === id)); + if (!isMatch) { + message = `【${config.name}】列存在所填选项不存在`; + } + } + + return message; +}; + +export const getValueRight = (value, type, config) => { + let objType = config.type === 'select' ? 'select[Range]' : 'int[Range]'; + objType = type !== 'in-range' ? type : objType; + const objValue = type === 'in-range' && config.type === 'int' ? { start: value[0], end: value[1] } : value; + const obj = { + type: objType, + value: objValue, + }; + return { + obj, + type: 'value', + }; +}; diff --git a/frontend/src/components/DecisionTable/components/TableHeader.vue b/frontend/src/components/DecisionTable/components/TableHeader.vue index 7f88501..ae07410 100644 --- a/frontend/src/components/DecisionTable/components/TableHeader.vue +++ b/frontend/src/components/DecisionTable/components/TableHeader.vue @@ -6,7 +6,25 @@ :title="item.name" :style="{ width: `${widthMap[item.type]}px` }" :class="['header-cell', { 'index-header': item.type === 'index' }]"> - @@ -87,13 +96,15 @@ import { mapActions } from 'vuex'; import tools from '@/utils/tools'; import TaskParamEdit from './components/TaskParamEdit.vue'; + import MockRecode from './components/MockRecode.vue'; export default { name: 'MockExecute', components: { TaskParamEdit, + MockRecode, }, props: { - headerLabel: { + mockTaskName: { type: String, default: '', }, @@ -173,8 +184,34 @@ this.isLoading = false; } }, + updateFormData(data) { + const { constants, mock_data_ids } = data; + + Object.entries(mock_data_ids).forEach(([key, value]) => { + const isMockExist = this.mockDataList[key]?.some(item => item.id === value); + if (key in this.mockFormData && isMockExist) { + this.mockFormData[key] = value; + } + }); + + const { taskParamEdit: paramEditComp } = this.$refs; + if (!paramEditComp) return; + + paramEditComp.renderData = Object.keys(constants).reduce((acc, key) => { + if (key in paramEditComp.renderData) { + acc[key] = constants[key].value; + } + return acc; + }, {}); + + this.$bkMessage({ + message: this.$t('复用成功'), + theme: 'success', + }); + }, async onCreateTask() { try { + if (!this.mockTaskName) return; const { taskParamEdit: paramEditComp, mockForm } = this.$refs; let validate = true; if (paramEditComp) { @@ -195,11 +232,16 @@ acc.nodes.push(cur); const mockInfo = this.mockDataList.find(item => item.id === value); acc.outputs[cur] = mockInfo ? mockInfo.data : {}; + acc.mock_data_ids[cur] = value; } return acc; - }, { nodes: [], outputs: {} }); + }, { + nodes: [], + outputs: {}, + mock_data_ids: {}, + }); const params = { - name: this.headerLabel, + name: this.mockTaskName, pipeline_tree: pipelineTree, mock_data: mockData, creator: this.creator, @@ -251,13 +293,17 @@ .mock-execute { flex: 1; display: flex; - flex-direction: column; max-height: calc(100% - 60px); background: #f5f7fa; - .form-wrapper { + .left-wrapper { flex: 1; - padding: 24px 270px; + display: flex; + flex-direction: column; + margin: 24px; + } + .form-wrapper { overflow-y: auto; + position: relative; @include scrollbar; .wrap-title { font-size: 14px; @@ -267,13 +313,18 @@ margin-bottom: 16px; } .variable-wrap { + height: 800px; padding: 16px 24px; margin-bottom: 16px; background: #fff; + box-shadow: 0 2px 4px 0 #1919290d; } .mock-wrap { + height: 600px; padding: 16px 24px; + margin-bottom: 4px; background: #fff; + box-shadow: 0 2px 4px 0 #1919290d; .bk-form { margin-bottom: 16px; } @@ -319,14 +370,12 @@ } } .action-wrapper { - height: 48px; - z-index: 2; - padding-left: 270px; - background: #fafbfd; - box-shadow: 0 -1px 0 0 #dcdee5; + position: sticky; + bottom: 0; + padding-top: 20px; + background: #f5f7fa; .bk-button { width: 88px; - margin-top: 8px; } } } diff --git a/frontend/src/views/template/TemplateMock/index.vue b/frontend/src/views/template/TemplateMock/index.vue index a369d38..9e1c35f 100644 --- a/frontend/src/views/template/TemplateMock/index.vue +++ b/frontend/src/views/template/TemplateMock/index.vue @@ -4,11 +4,12 @@ class="template-mock">
@@ -49,7 +50,7 @@ !item.optional); return hasSelected; }, - headerLabel() { - if (this.mockStep === 'setting') { - return this.tplName; - } - const nowTime = moment(new Date()).format('YYYYMMDDHHmmss'); - return `${this.tplName}_${this.$t('调试任务')}_${nowTime}`; - }, }, watch: { '$route.params': { From 62db74bf165cdfa3611b924b7edfdcf193ff5ffc Mon Sep 17 00:00:00 2001 From: waylon <1158341873@qq.com> Date: Mon, 21 Oct 2024 15:45:32 +0800 Subject: [PATCH 09/20] =?UTF-8?q?fix:=20=E5=A4=84=E7=90=86=20fast=5Fcreate?= =?UTF-8?q?=5Ftask=20=E9=80=9A=E7=9F=A5=E4=BA=BA=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20#39?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bkflow/apigw/serializers/task.py | 1 + bkflow/apigw/views/create_task_without_template.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bkflow/apigw/serializers/task.py b/bkflow/apigw/serializers/task.py index 6201330..fd88c3d 100644 --- a/bkflow/apigw/serializers/task.py +++ b/bkflow/apigw/serializers/task.py @@ -85,6 +85,7 @@ class CreateTaskWithoutTemplateSerializer(serializers.Serializer): description = serializers.CharField(help_text=_("任务描述"), required=False) constants = serializers.JSONField(help_text=_("任务启动参数"), required=False, default={}) pipeline_tree = serializers.JSONField(help_text=_("任务树"), required=True) + notify_config = serializers.JSONField(help_text=_("通知配置"), required=False, default={}) class PipelineTreeSerializer(serializers.Serializer): diff --git a/bkflow/apigw/views/create_task_without_template.py b/bkflow/apigw/views/create_task_without_template.py index bc70792..bff72fe 100644 --- a/bkflow/apigw/views/create_task_without_template.py +++ b/bkflow/apigw/views/create_task_without_template.py @@ -46,7 +46,8 @@ def create_task_without_template(request, space_id): "notify_type": {"fail": [], "success": []}, "notify_receivers": {"more_receiver": "", "receiver_group": []}, } - create_task_data.setdefault("extra_info", {}).update({"notify_config": DEFAULT_NOTIFY_CONFIG}) + notify_config = create_task_data.pop("notify_config", {}) or DEFAULT_NOTIFY_CONFIG + create_task_data.setdefault("extra_info", {}).update({"notify_config": notify_config}) client = TaskComponentClient(space_id=space_id) result = client.create_task(create_task_data) From 8dc37094439af6e96c11d338a45ee7ffd94bfd31 Mon Sep 17 00:00:00 2001 From: waylon <1158341873@qq.com> Date: Mon, 21 Oct 2024 15:48:15 +0800 Subject: [PATCH 10/20] =?UTF-8?q?feat:=20=E6=8E=A5=E5=8F=A3=20get=5Ftask?= =?UTF-8?q?=5Flist=20=E6=94=AF=E6=8C=81=E7=8A=B6=E6=80=81=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=20#41?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bkflow/apigw/views/get_task_list.py | 2 ++ bkflow/task/views.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bkflow/apigw/views/get_task_list.py b/bkflow/apigw/views/get_task_list.py index 3e9d872..0316e44 100644 --- a/bkflow/apigw/views/get_task_list.py +++ b/bkflow/apigw/views/get_task_list.py @@ -44,6 +44,8 @@ def get_task_list(request, space_id): "create_at_start": "create_time__gte", "create_at_end": "create_time__lte", "name": "name__icontains", + "is_started": "is_started", + "is_finished": "is_finished", } for k, v in filter_map.items(): if k in data: diff --git a/bkflow/task/views.py b/bkflow/task/views.py index 8fc07e9..3ecbf0c 100644 --- a/bkflow/task/views.py +++ b/bkflow/task/views.py @@ -73,6 +73,8 @@ class Meta: "start_time": ["gte", "lte"], "finish_time": ["gte", "lte"], "create_method": ["exact"], + "is_started": ["exact"], + "is_finished": ["exact"], } From 07ec2692e950a8778adb8b5ad97efab23cbcfb79 Mon Sep 17 00:00:00 2001 From: waylon <1158341873@qq.com> Date: Thu, 24 Oct 2024 11:48:52 +0800 Subject: [PATCH 11/20] =?UTF-8?q?feat:=20create=5Fspace=20api=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20APP=20=E7=99=BD=E5=90=8D=E5=8D=95=20#44?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bkflow/apigw/views/create_space.py | 3 ++- config/default.py | 2 ++ env.py | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/bkflow/apigw/views/create_space.py b/bkflow/apigw/views/create_space.py index 6f85605..467426d 100644 --- a/bkflow/apigw/views/create_space.py +++ b/bkflow/apigw/views/create_space.py @@ -21,6 +21,7 @@ from apigw_manager.apigw.decorators import apigw_require from blueapps.account.decorators import login_exempt +from django.conf import settings from django.db import transaction from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST @@ -46,7 +47,7 @@ def create_space(request): } """ data = json.loads(request.body) - if hasattr(request, "app"): + if hasattr(request, "app") and request.app.bk_app_code not in settings.APP_WHITE_LIST: data["app_code"] = request.app.bk_app_code ser = CreateSpaceSerializer(data=data) diff --git a/config/default.py b/config/default.py index 094d687..6788a60 100644 --- a/config/default.py +++ b/config/default.py @@ -85,6 +85,8 @@ APP_INTERNAL_FROM_SUPERUSER_HEADER_KEY = "Bkflow-Internal-From-SuperUser" APP_INTERNAL_TOKEN_REQUEST_META_KEY = "HTTP_BKFLOW_INTERNAL_TOKEN" +APP_WHITE_LIST = env.APP_WHITE_LIST_STR.split(",") if env.APP_WHITE_LIST_STR else [] + # PAAS SERVICE DETECTION BKPAAS_SERVICE_ADDRESSES_BKSAAS = os.getenv("BKPAAS_SERVICE_ADDRESSES_BKSAAS") BKSAAS_DEFAULT_MODULE_NAME = "default" diff --git a/env.py b/env.py index eac6a7e..11f0d90 100644 --- a/env.py +++ b/env.py @@ -131,3 +131,6 @@ # 文档中心链接 BK_DOC_CENTER_HOST = os.getenv("BK_DOC_CENTER_HOST", os.getenv("BK_DOCS_URL_PREFIX", "")).rstrip("/") + +# APP 白名单 +APP_WHITE_LIST_STR = os.getenv("BKAPP_APP_WHITE_LIST", "") # 逗号分隔的字符串 From a0c6285209054ae1b649e7f4ea60f412bccd0f14 Mon Sep 17 00:00:00 2001 From: guohelu <19503896967@163.com> Date: Mon, 28 Oct 2024 11:04:10 +0800 Subject: [PATCH 12/20] =?UTF-8?q?feat:=20http=20=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20json=20=E8=BD=AC=E4=B9=89=20#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bkflow/pipeline_plugins/components/collections/http/v1_0.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bkflow/pipeline_plugins/components/collections/http/v1_0.py b/bkflow/pipeline_plugins/components/collections/http/v1_0.py index be15c56..c36aa98 100644 --- a/bkflow/pipeline_plugins/components/collections/http/v1_0.py +++ b/bkflow/pipeline_plugins/components/collections/http/v1_0.py @@ -21,6 +21,7 @@ from __future__ import absolute_import +import json import traceback from copy import deepcopy @@ -133,6 +134,8 @@ def plugin_schedule(self, data, parent_data, callback_data=None): other = {"headers": {}, "timeout": timeout} if method.upper() not in ["GET", "HEAD"]: + if not isinstance(body, str): + body = json.dumps(body) other["data"] = body.encode("utf-8") other["headers"] = {"Content-type": "application/json"} From d2162cfc32454d0606aced8ec03a8273c0d01f50 Mon Sep 17 00:00:00 2001 From: waylon <1158341873@qq.com> Date: Tue, 29 Oct 2024 10:38:37 +0800 Subject: [PATCH 13/20] feat: release V1.9.0 #31 --- app_desc.yaml | 2 +- version_logs_md/V1.9.0_2024-10-29.md | 9 +++++++++ version_logs_md_en/V1.9.0_2024-10-29.md | 9 +++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 version_logs_md/V1.9.0_2024-10-29.md create mode 100644 version_logs_md_en/V1.9.0_2024-10-29.md diff --git a/app_desc.yaml b/app_desc.yaml index 9d1e2a3..cdd3329 100644 --- a/app_desc.yaml +++ b/app_desc.yaml @@ -1,5 +1,5 @@ spec_version: 2 -app_version: "1.8.1" +app_version: "1.9.0" app: region: default bk_app_code: &APP_CODE bk_flow_engine diff --git a/version_logs_md/V1.9.0_2024-10-29.md b/version_logs_md/V1.9.0_2024-10-29.md new file mode 100644 index 0000000..5b46b80 --- /dev/null +++ b/version_logs_md/V1.9.0_2024-10-29.md @@ -0,0 +1,9 @@ +# V1.9.0 版本更新说明 + +- [ 新增 ] 决策表支持导入导出 +- [ 新增 ] 接口 get_task_list 支持状态过滤 +- [ 新增 ] http 插件支持 json 转义 +- [ 优化 ] 调试任务相关交互优化 +- [ 优化 ] 出现全局公告时会额外出一个滚动条问题优化 +- [ 修复 ] 处理 fast_create_task 通知人覆盖问题 +- [ 修复 ] 日期时间范围变量值类型错误问题修复 diff --git a/version_logs_md_en/V1.9.0_2024-10-29.md b/version_logs_md_en/V1.9.0_2024-10-29.md new file mode 100644 index 0000000..28fd8db --- /dev/null +++ b/version_logs_md_en/V1.9.0_2024-10-29.md @@ -0,0 +1,9 @@ +# V1.9.0 Release Notes + +- [Feature] Support import and export of decision tables +- [Feature] API get_task_list now supports status filtering +- [Feature] HTTP plugin supports JSON escaping +- [Improvement] Improved debug task related interactions +- [Improvement] Fixed scrollbar issue when global announcement appears +- [Bugfix] Resolved notification recipient override issue in fast_create_task +- [Bugfix] Fixed date-time range variable type error From 2e9c0aa7d08bf9bea7b176a533bde54d60aa898e Mon Sep 17 00:00:00 2001 From: v_xugzhou <941071842@qq.com> Date: Wed, 6 Nov 2024 09:54:51 +0800 Subject: [PATCH 14/20] =?UTF-8?q?fix:=20=E8=B0=83=E8=AF=95=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E4=BA=A4=E4=BA=92=E9=AA=8C=E6=94=B6=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20--story=3D120517052=20#=20Reviewed,=20tran?= =?UTF-8?q?saction=20id:=2023086?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MockExecute/components/DiffDialog.vue | 28 ++------ .../MockExecute/components/MockRecode.vue | 70 ++++--------------- .../TemplateMock/MockExecute/index.vue | 9 ++- 3 files changed, 25 insertions(+), 82 deletions(-) diff --git a/frontend/src/views/template/TemplateMock/MockExecute/components/DiffDialog.vue b/frontend/src/views/template/TemplateMock/MockExecute/components/DiffDialog.vue index b53de67..ab03fa6 100644 --- a/frontend/src/views/template/TemplateMock/MockExecute/components/DiffDialog.vue +++ b/frontend/src/views/template/TemplateMock/MockExecute/components/DiffDialog.vue @@ -32,18 +32,10 @@ - - + {{ item.left.name ? `${item.left.node_name}: ${item.left.name}` : '--' }} + + {{ `${item.right.node_name}: ID 为 【${item.right.id}】 的 mock 方案不存在` }} + @@ -63,18 +55,6 @@ default: () => ([]), }, }, - data() { - return { - - }; - }, - methods: { - getTdName(data) { - let name = data.name ? `${data.node_name}: ${data.name}` : `${data.node_name}: ID 为 【${data.id}】 的 mock 方案不存在`; - name = !data.node_name ? '--' : name; - return name; - }, - }, };