diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/StartParamListValidator.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/StartParamListValidator.java new file mode 100644 index 000000000000..e59aacfea15e --- /dev/null +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/StartParamListValidator.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +package org.apache.dolphinscheduler.api.validator; + +import org.apache.dolphinscheduler.plugin.task.api.model.Property; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Component; + +/** + * Validator for the list of start parameters (Property list). + *

If startParamList is not valid, an {@link IllegalArgumentException} will be thrown.

+ */ +@Slf4j +@Component +public class StartParamListValidator implements IValidator> { + + @Override + public void validate(List startParamList) { + if (CollectionUtils.isEmpty(startParamList)) { + return; + } + + Set keys = new HashSet<>(); + for (Property param : startParamList) { + if (StringUtils.isBlank(param.getProp())) { + throw new IllegalArgumentException("Parameter key cannot be empty"); + } + + String key = param.getProp().trim(); + if (keys.contains(key)) { + throw new IllegalArgumentException("Duplicate parameter key: " + key); + } + keys.add(key); + } + } +} diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/BackfillWorkflowDTOValidator.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/BackfillWorkflowDTOValidator.java index 0b5da7e85a3f..08fa5c2b2b8a 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/BackfillWorkflowDTOValidator.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/validator/workflow/BackfillWorkflowDTOValidator.java @@ -18,6 +18,7 @@ package org.apache.dolphinscheduler.api.validator.workflow; import org.apache.dolphinscheduler.api.validator.IValidator; +import org.apache.dolphinscheduler.api.validator.StartParamListValidator; import org.apache.dolphinscheduler.api.validator.TenantExistValidator; import org.apache.dolphinscheduler.common.enums.CommandType; import org.apache.dolphinscheduler.common.enums.ReleaseState; @@ -34,8 +35,12 @@ public class BackfillWorkflowDTOValidator implements IValidator startParamListValidator.validate(null)) + .doesNotThrowAnyException(); + } + + @Test + void testValidate_emptyList() { + assertThatCode(() -> startParamListValidator.validate(Collections.emptyList())) + .doesNotThrowAnyException(); + } + + @Test + void testValidate_validParameters() { + List params = Collections.singletonList( + new Property("workflow_id", Direct.IN, DataType.VARCHAR, "1001")); + assertThatCode(() -> startParamListValidator.validate(params)) + .doesNotThrowAnyException(); + } + + @Test + void testValidate_rejectsBlankOrEmptyKeys() { + assertThrowsIllegalArgument(""); + + assertThrowsIllegalArgument(" "); + + assertThrowsIllegalArgument("\t"); + + assertThrowsIllegalArgument("\n"); + + assertThrowsIllegalArgument(" \t\n "); + } + + private void assertThrowsIllegalArgument(String propValue) { + List params = Collections.singletonList( + new Property(propValue, Direct.IN, DataType.VARCHAR, "dummyValue")); + + assertThatThrownBy(() -> startParamListValidator.validate(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Parameter key cannot be empty"); + } + + @Test + void testValidate_duplicateKeys() { + List params = new ArrayList<>(); + params.add(new Property("app_name", Direct.IN, DataType.VARCHAR, "A")); + params.add(new Property("app_name", Direct.IN, DataType.VARCHAR, "B")); + + assertThatThrownBy(() -> startParamListValidator.validate(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Duplicate parameter key: app_name"); + } + + @Test + void testValidate_duplicateKeysAfterTrim() { + List params = new ArrayList<>(); + params.add(new Property(" app_name ", Direct.IN, DataType.VARCHAR, "A")); + params.add(new Property("app_name", Direct.IN, DataType.VARCHAR, "B")); + + assertThatThrownBy(() -> startParamListValidator.validate(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Duplicate parameter key: app_name"); + } + + @Test + void testValidate_inTypeEmptyValueAllowed() { + List params = Collections.singletonList( + new Property("input_var", Direct.IN, DataType.VARCHAR, "")); + + assertThatCode(() -> startParamListValidator.validate(params)) + .doesNotThrowAnyException(); + } + + @Test + void testValidate_outTypeEmptyValueAllowed() { + List params = Collections.singletonList( + new Property("output_var", Direct.OUT, DataType.VARCHAR, "")); + + assertThatCode(() -> startParamListValidator.validate(params)) + .doesNotThrowAnyException(); + } +} diff --git a/dolphinscheduler-ui/src/locales/en_US/project.ts b/dolphinscheduler-ui/src/locales/en_US/project.ts index 30ea28d8f421..59edc7dfeaed 100644 --- a/dolphinscheduler-ui/src/locales/en_US/project.ts +++ b/dolphinscheduler-ui/src/locales/en_US/project.ts @@ -353,8 +353,8 @@ export default { update_directly: 'Whether to update the workflow definition', dag_name_empty: 'DAG graph name cannot be empty', positive_integer: 'Please enter a positive integer greater than 0', - prop_empty: 'prop is empty', - prop_repeat: 'prop is repeat', + prop_key_repeat: 'prop key is repeat', + prop_key_empty: 'prop key is empty', node_not_created: 'Failed to save node not created', copy_name: 'Copy Name', view_variables: 'View Variables', diff --git a/dolphinscheduler-ui/src/locales/zh_CN/project.ts b/dolphinscheduler-ui/src/locales/zh_CN/project.ts index 82255264bdd4..e6b08093c568 100644 --- a/dolphinscheduler-ui/src/locales/zh_CN/project.ts +++ b/dolphinscheduler-ui/src/locales/zh_CN/project.ts @@ -347,8 +347,8 @@ export default { update_directly: '是否更新工作流定义', dag_name_empty: 'DAG图名称不能为空', positive_integer: '请输入大于 0 的正整数', - prop_empty: '自定义参数prop不能为空', - prop_repeat: 'prop中有重复', + prop_key_repeat: '自定义参数prop中key有重复', + prop_key_empty: '自定义参数prop中key不能为空', node_not_created: '未创建节点保存失败', copy_name: '复制名称', view_variables: '查看变量', diff --git a/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag-save-modal.tsx b/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag-save-modal.tsx index a6b78cda384c..1e39a6d37e4e 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag-save-modal.tsx +++ b/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag-save-modal.tsx @@ -98,23 +98,23 @@ export default defineComponent({ }, globalParams: { validator() { - const props = new Set() + const globalParams = formValue.value.globalParams || [] - const keys = formValue.value.globalParams.map((item) => item.key) - const keysSet = new Set(keys) - if (keysSet.size !== keys.length) { - return new Error(t('project.dag.prop_repeat')) - } + if (!globalParams || globalParams.length === 0) return true - for (const param of formValue.value.globalParams) { - const prop = param.value - const direct = param.direct - if (direct === 'IN' && !prop) { - return new Error(t('project.dag.prop_empty')) + for (const param of globalParams) { + if (!param.key || param.key.trim() === '') { + return new Error(t('project.dag.prop_key_empty')) } + } - props.add(prop) + const keys = globalParams.map((item) => (item.key || '').trim()) + const uniqueKeys = new Set(keys) + if (uniqueKeys.size !== keys.length) { + return new Error(t('project.dag.prop_key_repeat')) } + + return true } } } diff --git a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/start-modal.tsx b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/start-modal.tsx index bb574d2e5045..eea89ad0d4e2 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/start-modal.tsx +++ b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/start-modal.tsx @@ -290,6 +290,11 @@ export default defineComponent({ } ) + const formModel = computed(() => ({ + ...startState.startForm, + startParamsList: variables.startParamsList + })) + return { t, showTaskDependType, @@ -302,6 +307,7 @@ export default defineComponent({ removeStartParams, addStartParams, updateParamsList, + formModel, ...toRefs(variables), ...toRefs(startState), ...toRefs(props), @@ -319,7 +325,7 @@ export default defineComponent({ onConfirm={this.handleStart} confirmLoading={this.saving} > - + { return { - key: '', + prop: '', direct: 'IN', type: 'VARCHAR', value: '' diff --git a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-form.ts b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-form.ts index 357531129535..f4955a9d990d 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-form.ts +++ b/dolphinscheduler-ui/src/views/projects/workflow/definition/components/use-form.ts @@ -101,6 +101,28 @@ export const useForm = () => { return new Error(t('project.workflow.warning_group_tip')) } } + }, + startParamsList: { + trigger: ['input', 'blur'], + validator: (rule: any, value: Array) => { + const params = value || [] + + if (!params || params.length === 0) return true + + for (const param of params) { + if (!param.prop || param.prop.trim() === '') { + return new Error(t('project.dag.prop_key_empty')) + } + } + + const keys = params.map((item) => (item.prop || '').trim()) + const uniqueKeys = new Set(keys) + if (uniqueKeys.size !== keys.length) { + return new Error(t('project.dag.prop_key_repeat')) + } + + return true + } } } })