diff --git a/app_desc.yaml b/app_desc.yaml index fcc4a66..cdd3329 100644 --- a/app_desc.yaml +++ b/app_desc.yaml @@ -1,5 +1,5 @@ spec_version: 2 -app_version: "1.8.0" +app_version: "1.9.0" app: region: default bk_app_code: &APP_CODE bk_flow_engine diff --git a/bkflow/interface/context_processors.py b/bkflow/interface/context_processors.py index 9eedfc1..2b66dff 100644 --- a/bkflow/interface/context_processors.py +++ b/bkflow/interface/context_processors.py @@ -27,6 +27,7 @@ def bkflow_settings(request): frontend_entry_url = "{}bkflow".format(settings.STATIC_URL) if settings.RUN_VER == "open" else "/static/bkflow" enable_notice_center = int(EnvironmentVariables.objects.get_var("ENABLE_NOTICE_CENTER", 0)) language = request.COOKIES.get("blueking_language", "zh-cn") + doc_lang_mappings = {"zh-cn": "ZH", "en": "EN"} run_ver_key = "BKAPP_RUN_VER_NAME" if language == "zh-cn" else "BKAPP_RUN_VER_NAME_{}".format(language.upper()) ctx = { @@ -36,7 +37,8 @@ def bkflow_settings(request): "MEMBER_SELECTOR_DATA_HOST": settings.MEMBER_SELECTOR_DATA_HOST, "APP_CODE": settings.APP_CODE, "USERNAME": request.user.username, - "BK_DOC_URL": f"{env.BK_DOC_CENTER_HOST}/markdown/ZH/BKFlow/1.8/UserGuide/Introduce/introduce.md", + "BK_DOC_URL": f"{env.BK_DOC_CENTER_HOST}/markdown/{doc_lang_mappings.get(language, 'ZH')}/BKFlow/1.8" + f"/UserGuide/Introduce/introduce.md", # 是否开启通知中心 "ENABLE_NOTICE_CENTER": enable_notice_center, "BK_PAAS_SHARED_RES_URL": env.BKPAAS_SHARED_RES_URL, diff --git a/bkflow/pipeline_plugins/query/select.py b/bkflow/pipeline_plugins/query/select.py index aab868b..5522134 100644 --- a/bkflow/pipeline_plugins/query/select.py +++ b/bkflow/pipeline_plugins/query/select.py @@ -21,55 +21,54 @@ import logging -import requests -from django.conf import settings from django.http import JsonResponse -from django.utils.translation import ugettext_lazy as _ logger = logging.getLogger("root") def variable_select_source_data_proxy(request): """ - @summary: 获取下拉框源数据的通用接口 + @summary: 获取下拉框源数据的通用接口,暂时关闭该接口 @param request: @return: """ - url = request.GET.get("url") - try: - response = requests.get(url=url, verify=False, timeout=10) - except Exception as e: - logger.exception("variable select get data from url[url={url}] raise error: {error}".format(url=url, error=e)) - text = _("请求数据异常: {error}").format(error=e) - data = [{"text": text, "value": ""}] - return JsonResponse(data, safe=False) + return JsonResponse([{"text": "text1", "value": "value1"}]) - try: - data = response.json() - except Exception: - try: - content = response.content.decode(response.encoding) - logger.exception( - "variable select get data from url[url={url}] is not a valid JSON: {data}".format( - url=url, data=content[:500] - ) - ) - except Exception: - logger.exception("variable select get data from url[url={url}] data is not a valid JSON".format(url=url)) - text = _("返回数据格式错误,不是合法 JSON 格式") - data = [{"text": text, "value": ""}] - return JsonResponse(data, safe=False) - - # 支持开发者对远程数据源数据配置处理函数,进行再处理 - post_process_function = getattr(settings, "REMOTE_SOURCE_DATA_TRANSFORM_FUNCTION", None) - if post_process_function and callable(post_process_function): - try: - data = post_process_function(data) - except Exception as e: - logger.exception( - "variable select transforming data from remote resource url[url={url}] " - "raise error: {error}".format(url=url, error=e) - ) - text = _("远程数据源数据转换失败: {error}").format(error=e) - data = [{"text": text, "value": ""}] - return JsonResponse(data, safe=False) + # url = request.GET.get("url") + # try: + # response = requests.get(url=url, verify=False, timeout=10) + # except Exception as e: + # logger.exception("variable select get data from url[url={url}] raise error: {error}".format(url=url, error=e)) + # text = _("请求数据异常: {error}").format(error=e) + # data = [{"text": text, "value": ""}] + # return JsonResponse(data, safe=False) + # + # try: + # data = response.json() + # except Exception: + # try: + # content = response.content.decode(response.encoding) + # logger.exception( + # "variable select get data from url[url={url}] is not a valid JSON: {data}".format( + # url=url, data=content[:500] + # ) + # ) + # except Exception: + # logger.exception("variable select get data from url[url={url}] data is not a valid JSON".format(url=url)) + # text = _("返回数据格式错误,不是合法 JSON 格式") + # data = [{"text": text, "value": ""}] + # return JsonResponse(data, safe=False) + # + # # 支持开发者对远程数据源数据配置处理函数,进行再处理 + # post_process_function = getattr(settings, "REMOTE_SOURCE_DATA_TRANSFORM_FUNCTION", None) + # if post_process_function and callable(post_process_function): + # try: + # data = post_process_function(data) + # except Exception as e: + # logger.exception( + # "variable select transforming data from remote resource url[url={url}] " + # "raise error: {error}".format(url=url, error=e) + # ) + # text = _("远程数据源数据转换失败: {error}").format(error=e) + # data = [{"text": text, "value": ""}] + # return JsonResponse(data, safe=False) diff --git a/bkflow/pipeline_plugins/static/variables/select.js b/bkflow/pipeline_plugins/static/variables/select.js index 80da7db..d5eafa0 100644 --- a/bkflow/pipeline_plugins/static/variables/select.js +++ b/bkflow/pipeline_plugins/static/variables/select.js @@ -16,6 +16,7 @@ hookable: true, items: [{name: gettext("自定义"), value: "0"}, {name: gettext("远程数据源"), value: "1"}], value: "0", + hidden: true, validation: [ { type: "required" @@ -29,7 +30,7 @@ attrs: { name: gettext("选项"), hookable: true, - placeholder: gettext('请输入数据源信息,自定义数据源格式为 [{"text": "", "value": ""}...],若是远程数据源则填写返回该格式数据的 URL'), + placeholder: gettext('请输入数据源信息,自定义数据源格式为 [{"text": "", "value": ""}...]'), validation: [ { type: "required" @@ -73,10 +74,11 @@ let remote_url = ""; let items = []; let placeholder = ''; - if (metaConfig.datasource === "1") { - remote_url = $.context.get('site_url') + 'api/plugin_query/variable_select_source_data_proxy/?url=' + metaConfig.items_text; - remote = true; - } else { + // if (metaConfig.datasource === "1") { + // remote_url = $.context.get('site_url') + 'api/plugin_query/variable_select_source_data_proxy/?url=' + metaConfig.items_text; + // remote = true; + // } + if (metaConfig.datasource === "0") { try { items = JSON.parse(metaConfig.items_text); } catch (err) { @@ -92,16 +94,16 @@ let multiple = false; let default_val = metaConfig.default || ''; - if (metaConfig.type === "1") { - multiple = true; - default_val = []; - if (metaConfig.default) { - let vals = metaConfig.default.split(','); - for (let i in vals) { - default_val.push(vals[i].trim()); - } - } - } + // if (metaConfig.type === "1") { + // multiple = true; + // default_val = []; + // if (metaConfig.default) { + // let vals = metaConfig.default.split(','); + // for (let i in vals) { + // default_val.push(vals[i].trim()); + // } + // } + // } return { tag_code: this.tag_code, type: "select", @@ -115,7 +117,7 @@ remote_url: remote_url, placeholder: placeholder, remote_data_init: function (data) { - return data + return data; }, validation: [ { diff --git a/bkflow/space/migrations/0007_alter_space_app_code.py b/bkflow/space/migrations/0007_alter_space_app_code.py new file mode 100644 index 0000000..a2f7418 --- /dev/null +++ b/bkflow/space/migrations/0007_alter_space_app_code.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.15 on 2024-10-09 03:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('space', '0006_auto_20240823_1544'), + ] + + operations = [ + migrations.AlterField( + model_name='space', + name='app_code', + field=models.CharField(max_length=32, verbose_name='应用ID'), + ), + ] diff --git a/bkflow/space/models.py b/bkflow/space/models.py index 5da8d5f..a03d95e 100644 --- a/bkflow/space/models.py +++ b/bkflow/space/models.py @@ -58,7 +58,7 @@ class Space(CommonModel): id = models.AutoField(_("空间ID"), primary_key=True) # 空间名不允许重复 name = models.CharField(_("空间名称"), max_length=32, null=False, blank=False, unique=True) - app_code = models.CharField(_("APP Code"), max_length=32, null=False, blank=False) + app_code = models.CharField(_("应用ID"), max_length=32, null=False, blank=False) desc = models.CharField(_("空间描述"), max_length=128, null=True, blank=True) platform_url = models.CharField(_("平台提供服务的地址"), max_length=256, null=False, blank=False) create_type = models.CharField( diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 37cbf87..45924c7 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -192,6 +192,7 @@ body { } .with-system-notice { height: calc(100vh - 40px) !important; + .navigation-nav, .container-content { max-height: calc(100vh - 92px)!important; } diff --git a/frontend/src/components/DecisionTable/ImportExport/ExportBtn.vue b/frontend/src/components/DecisionTable/ImportExport/ExportBtn.vue index 9967c26..a971fd8 100644 --- a/frontend/src/components/DecisionTable/ImportExport/ExportBtn.vue +++ b/frontend/src/components/DecisionTable/ImportExport/ExportBtn.vue @@ -13,10 +13,6 @@ import { getCellText } from './dataTransfer.js'; export default { props: { - name: { - type: String, - default: '', - }, data: { type: Object, default: () => ({}), @@ -42,34 +38,52 @@ horizontal: 'center', // 水平居中 vertical: 'center', // 垂直居中 }, - border: { - top: { style: 'thin', color: { rgb: '000000' } }, - bottom: { style: 'thin', color: { rgb: '000000' } }, - left: { style: 'thin', color: { rgb: '000000' } }, - right: { style: 'thin', color: { rgb: '000000' } }, - }, + border: ['top', 'bottom', 'left', 'right'].reduce((acc, side) => { + acc[side] = { style: 'thin', color: { rgb: '000000' } }; + return acc; + }, {}), }; // 定义表头和注释 const headers = [ { label: 'Input', - children: inputs.map(item => ({ label: `${item.name}(${item.id})`, description: JSON.stringify(item) })), + children: inputs.map((item) => { + const { id, name, ...rest } = item; + return { label: `${name}(${id})`, description: JSON.stringify(rest) }; + }), }, { label: 'Output', - children: outputs.map(item => ({ label: `${item.name}(${item.id})`, description: JSON.stringify(item) })), + children: outputs.map((item) => { + const { id, name, ...rest } = item; + return { label: `${name}(${id})`, description: JSON.stringify(rest) }; + }), }, ]; const data = records.reduce((acc, cur) => { // 暂时过滤【条件组合】类型!!! if (cur.inputs.type !== 'common') return acc; const arr = []; - cur.inputs.conditions.forEach((item) => { - arr.push({ v: getCellText(item), t: 's', s: cellStyles }); + cur.inputs.conditions.forEach((item, index) => { + const inputInfo = inputs[index]; + const v = getCellText(item); + if (inputInfo.type === 'select') { + // 下拉框类型导出为text + const option = inputInfo.options.items.find(o => o.id === v); + arr.push({ v: option.name, t: 's', s: cellStyles }); + } else { + arr.push({ v, t: 's', s: cellStyles }); + } }); outputs.forEach((item) => { - arr.push({ v: cur.outputs[item.id], t: 's', s: cellStyles }); + if (item.type === 'select') { + // 下拉框类型导出为text + const option = item.options.items.find(o => o.id === cur.outputs[item.id]); + arr.push({ v: option.name, t: 's', s: cellStyles }); + } else { + arr.push({ v: cur.outputs[item.id], t: 's', s: cellStyles }); + } }); acc.push(arr); return acc; @@ -108,8 +122,9 @@ fill: { fgColor: { rgb: '9FE3FF' } }, }, }); + const c = headerIndex === 0 ? childIndex : (headers[headerIndex - 1].children.length + childIndex); comments.push({ - cell: XLSX.utils.encode_cell({ c: headerIndex * header.children.length + childIndex, r: 1 }), + cell: XLSX.utils.encode_cell({ c, r: 1 }), comment: child.description, }); }); @@ -154,7 +169,7 @@ XLSX.utils.book_append_sheet(wb, ws, 'Sheet1'); // 导出工作簿 - XLSX.writeFile(wb, `${this.name || 'Decision'}_${moment().format('YYYYMMDDHHmmss')}.xlsx`); + XLSX.writeFile(wb, `${window.APP_CODE}_decision_${moment().format('YYYYMMDDHHmmss')}.xlsx`); }, }, }; diff --git a/frontend/src/components/DecisionTable/ImportExport/ImportBtn.vue b/frontend/src/components/DecisionTable/ImportExport/ImportBtn.vue index 4e0496b..c0a60d2 100644 --- a/frontend/src/components/DecisionTable/ImportExport/ImportBtn.vue +++ b/frontend/src/components/DecisionTable/ImportExport/ImportBtn.vue @@ -17,7 +17,7 @@ + diff --git a/frontend/src/views/template/TemplateMock/MockExecute/components/MockRecode.vue b/frontend/src/views/template/TemplateMock/MockExecute/components/MockRecode.vue new file mode 100644 index 0000000..77f9de8 --- /dev/null +++ b/frontend/src/views/template/TemplateMock/MockExecute/components/MockRecode.vue @@ -0,0 +1,298 @@ + + + + +./DiffDialog.vue diff --git a/frontend/src/views/template/TemplateMock/MockExecute/index.vue b/frontend/src/views/template/TemplateMock/MockExecute/index.vue index 1f7f6f2..7507862 100644 --- a/frontend/src/views/template/TemplateMock/MockExecute/index.vue +++ b/frontend/src/views/template/TemplateMock/MockExecute/index.vue @@ -2,84 +2,93 @@
-
-
-

- {{ $t('填写调试入参') }} -

- -
-
-

- {{ $t('选择 Mock 数据') }} -

- - + + +
+
+
+ + {{ $t('执行') }} + + + {{ $t('取消') }} +
-
- - {{ $t('执行') }} - - - {{ $t('取消') }} - -
+ @@ -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.some(item => item.id === value && item.node_id === key); + 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,20 @@ .mock-execute { flex: 1; display: flex; - flex-direction: column; max-height: calc(100% - 60px); background: #f5f7fa; + .left-wrapper { + flex: 1; + display: flex; + flex-direction: column; + margin: 24px; + } .form-wrapper { + display: flex; + flex-direction: column; flex: 1; - padding: 24px 270px; overflow-y: auto; + position: relative; @include scrollbar; .wrap-title { font-size: 14px; @@ -267,13 +316,18 @@ margin-bottom: 16px; } .variable-wrap { + flex: 1; padding: 16px 24px; margin-bottom: 16px; background: #fff; + box-shadow: 0 2px 4px 0 #1919290d; } .mock-wrap { + flex: 1; padding: 16px 24px; + margin-bottom: 4px; background: #fff; + box-shadow: 0 2px 4px 0 #1919290d; .bk-form { margin-bottom: 16px; } @@ -319,14 +373,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': { diff --git a/module_settings.py b/module_settings.py index 5f249e4..fd80274 100644 --- a/module_settings.py +++ b/module_settings.py @@ -242,7 +242,7 @@ def check_engine_admin_permission(request, *args, **kwargs): BK_APIGW_GRANT_APPS = env.BK_APIGW_GRANT_APPS # version log config - VERSION_LOG = {"FILE_TIME_FORMAT": "%Y-%m-%d"} + VERSION_LOG = {"FILE_TIME_FORMAT": "%Y-%m-%d", "LANGUAGE_MAPPINGS": {"en": "en"}} # bk notice config DISABLE_REGISTER_BKFLOW_TO_BKNOTICE = env.DISABLE_REGISTER_BKFLOW_TO_BKNOTICE diff --git a/scripts/start_new_version.sh b/scripts/start_new_version.sh index bb63906..49d5f1b 100644 --- a/scripts/start_new_version.sh +++ b/scripts/start_new_version.sh @@ -4,17 +4,15 @@ RELEASE_VERSION=$1 C_OS="$(uname)" MAC_OS="Darwin" -ver=$(grep 'app_version:' app_desc_ce.yaml | cut -c 14-) +ver=$(grep 'app_version:' app_desc.yaml | cut -c 14-) echo "current version: ${ver}" # version num replace if [ "$C_OS" == "$MAC_OS" ];then echo "app_version: \"${ver}\"" - sed -i.bak "s/app_version: ${ver}/app_version: \"${RELEASE_VERSION}\"/" app_desc_ce.yaml - sed -i.bak "s/app_version: ${ver}/app_version: \"${RELEASE_VERSION}\"/" app_desc_sg.yaml - rm app_desc_ce.yaml.bak app_desc_sg.yaml.bak + sed -i.bak "s/app_version: ${ver}/app_version: \"${RELEASE_VERSION}\"/" app_desc.yaml + rm app_desc.yaml.bak else - sed -i "s/app_version: ${ver}/app_version: \"${RELEASE_VERSION}\"/" app_desc_ce.yaml - sed -i "s/app_version: ${ver}/app_version: \"${RELEASE_VERSION}\"/" app_desc_sg.yaml + sed -i "s/app_version: ${ver}/app_version: \"${RELEASE_VERSION}\"/" app_desc.yaml fi 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.4.0_2024-01-24.md b/version_logs_md_en/V1.4.0_2024-01-24.md new file mode 100644 index 0000000..fe2d48d --- /dev/null +++ b/version_logs_md_en/V1.4.0_2024-01-24.md @@ -0,0 +1,7 @@ +# V1.4.0 Release Notes + +- [ Feature ] Support for embedded process orchestration canvas +- [ Feature ] Support for process task execution +- [ Feature ] Support for highly extensible & customizable features +- [ Feature ] Support for multiple access methods +- [ Feature ] Support for multiple integration methods \ No newline at end of file diff --git a/version_logs_md_en/V1.5.0_2024-02-22.md b/version_logs_md_en/V1.5.0_2024-02-22.md new file mode 100644 index 0000000..8ee73eb --- /dev/null +++ b/version_logs_md_en/V1.5.0_2024-02-22.md @@ -0,0 +1,6 @@ +# V1.5.0 Release Notes + +- [ Feature ] Support for vertical process canvas +- [ Feature ] Mock functionality for processes +- [ Feature ] Support for webhook module +- [ Feature ] Support for BlueKing plugin space call limit \ No newline at end of file diff --git a/version_logs_md_en/V1.6.0_2024-03-27.md b/version_logs_md_en/V1.6.0_2024-03-27.md new file mode 100644 index 0000000..1b72d0a --- /dev/null +++ b/version_logs_md_en/V1.6.0_2024-03-27.md @@ -0,0 +1,4 @@ +# V1.6.0 Release Notes + +- [ Feature ] Management interface supports quick space configuration for space administrators +- [ Feature ] Management interface supports quick access to space and module information for system administrators \ No newline at end of file diff --git a/version_logs_md_en/V1.7.0_2024-06-06.md b/version_logs_md_en/V1.7.0_2024-06-06.md new file mode 100644 index 0000000..53c2eef --- /dev/null +++ b/version_logs_md_en/V1.7.0_2024-06-06.md @@ -0,0 +1,8 @@ +# V1.7.0 Release Notes + +- [ Feature ] BKFlow welcome introduction page +- [ Feature ] Management interface supports users creating new spaces to experience full functionality +- [ Feature ] Support for decision table functionality based on processes +- [ Improvement ] Management interface interaction optimization +- [ Improvement ] Management interface space list optimization +- [ Bugfix ] Fixed several bugs diff --git a/version_logs_md_en/V1.8.0_2024-08-01.md b/version_logs_md_en/V1.8.0_2024-08-01.md new file mode 100644 index 0000000..a9c4af4 --- /dev/null +++ b/version_logs_md_en/V1.8.0_2024-08-01.md @@ -0,0 +1,5 @@ +# V1.8.0 Release Notes + +- [ Feature ] Page support for internationalization +- [ Feature ] Community version functionality support +- [ Bugfix ] Fixed several bugs \ No newline at end of file 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