From 84b9a74c6ea3e2f983c988ccad203796370e77fe Mon Sep 17 00:00:00 2001 From: YannC <37600690+Skraye@users.noreply.github.com> Date: Tue, 11 Apr 2023 10:31:45 +0200 Subject: [PATCH] feat(ui): add validation on lowcode everywhere (#1139) - Added task validation at the creation - Added trigger validation - Added validation in "New Error" section --- ui/src/components/flows/TaskEditor.vue | 2 +- ui/src/components/graph/Topology.vue | 23 +++++++++++++++++-- ui/src/components/graph/nodes/Edge.vue | 16 +++++++++++-- ui/src/stores/flow.js | 2 +- .../webserver/controllers/FlowController.java | 15 +++++++++--- 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/ui/src/components/flows/TaskEditor.vue b/ui/src/components/flows/TaskEditor.vue index 569a9c6ae11..e52a6cac655 100644 --- a/ui/src/components/flows/TaskEditor.vue +++ b/ui/src/components/flows/TaskEditor.vue @@ -43,7 +43,7 @@ if (this.modelValue) { this.taskObject = YamlUtils.parse(this.modelValue); this.selectedTaskType = this.taskObject.type; - this.$store.dispatch("flow/validateTask", {task: this.modelValue}) + this.$store.dispatch("flow/validateTask", {task: this.modelValue, section: this.section}) this.load(); } diff --git a/ui/src/components/graph/Topology.vue b/ui/src/components/graph/Topology.vue index 34bd9f907bb..e132306ccc2 100644 --- a/ui/src/components/graph/Topology.vue +++ b/ui/src/components/graph/Topology.vue @@ -13,6 +13,7 @@ import BottomLine from "../../components/layout/BottomLine.vue"; import TriggerFlow from "../../components/flows/TriggerFlow.vue"; + import ValidationError from "../../components/flows/ValidationError.vue"; import SwitchView from "./SwitchView.vue"; import {cssVariable} from "../../utils/global" import Cluster from "./nodes/Cluster.vue"; @@ -121,6 +122,11 @@ const updatedFromEditor = ref(false); const timer = ref(null); const dragging = ref(false); + const taskError = ref(store.getters["flow/taskError"]) + + watch(() => store.getters["flow/taskError"], async () => { + taskError.value = store.getters["flow/taskError"]; + }); const flowables = () => { @@ -562,6 +568,11 @@ } const onUpdateNewTrigger = (event) => { + clearTimeout(timer.value); + timer.value = setTimeout(() => store.dispatch("flow/validateTask", { + task: event, + section: "trigger" + }), 500); newTrigger.value = event; } @@ -583,6 +594,12 @@ } const onUpdateNewError = (event) => { + clearTimeout(timer.value); + timer.value = setTimeout(() => store.dispatch("flow/validateTask", { + task: event, + section: "task" + }), 500); + newError.value = event; } @@ -869,7 +886,8 @@ /> @@ -889,7 +907,8 @@ /> diff --git a/ui/src/components/graph/nodes/Edge.vue b/ui/src/components/graph/nodes/Edge.vue index 66107aabbd0..9be4a55ac2c 100644 --- a/ui/src/components/graph/nodes/Edge.vue +++ b/ui/src/components/graph/nodes/Edge.vue @@ -2,7 +2,7 @@ import type {EdgeProps, Position} from '@vue-flow/core' import {EdgeLabelRenderer, getSmoothStepPath, useEdge} from '@vue-flow/core' import type {CSSProperties} from 'vue' - import {computed, getCurrentInstance, ref} from 'vue' + import {computed, getCurrentInstance, ref, watch} from 'vue' import TaskEditor from "../../flows/TaskEditor.vue" import Help from "vue-material-design-icons/Help.vue"; import HelpCircle from "vue-material-design-icons/HelpCircle.vue"; @@ -15,6 +15,7 @@ import yamlUtils from "../../../utils/yamlUtils.js"; import YamlUtils from "../../../utils/yamlUtils.js"; import {useStore} from "vuex"; + import ValidationError from "../../flows/ValidationError.vue"; const store = useStore(); const t = getCurrentInstance().appContext.config.globalProperties.$t; @@ -41,6 +42,12 @@ const emit = defineEmits(["edit"]) const taskYaml = ref(""); const execution = store.getters["execution/execution"]; + const timer = ref(undefined); + const taskError = ref(store.getters["flow/taskError"]) + + watch(() => store.getters["flow/taskError"], async () => { + taskError.value = store.getters["flow/taskError"]; + }); const isBorderEdge = () => { if (!props.data.haveAdd && props.data.isFlowable) { @@ -104,6 +111,10 @@ const updateTask = (task) => { taskYaml.value = task; + clearTimeout(timer.value); + timer.value = setTimeout(() => { + store.dispatch("flow/validateTask", {task: task}) + }, 500); } const getAddTaskInformation = () => { @@ -237,7 +248,8 @@ /> diff --git a/ui/src/stores/flow.js b/ui/src/stores/flow.js index 5b6fc65ca1f..a5b7e646f7d 100644 --- a/ui/src/stores/flow.js +++ b/ui/src/stores/flow.js @@ -212,7 +212,7 @@ export default { }) }, validateTask({commit}, options) { - return axios.post(`${apiRoot}flows/validate/task`, options.task, textYamlHeader) + return axios.post(`${apiRoot}flows/validate/task`, options.task, {...textYamlHeader, params: {section: options.section ? options.section : "task"}}) .then(response => { commit("setTaskError", response.data.constraints) return response.data diff --git a/webserver/src/main/java/io/kestra/webserver/controllers/FlowController.java b/webserver/src/main/java/io/kestra/webserver/controllers/FlowController.java index 21cdfacd476..eb4514ead5f 100644 --- a/webserver/src/main/java/io/kestra/webserver/controllers/FlowController.java +++ b/webserver/src/main/java/io/kestra/webserver/controllers/FlowController.java @@ -9,6 +9,7 @@ import io.kestra.core.models.tasks.Task; import io.kestra.core.models.topologies.FlowTopology; import io.kestra.core.models.topologies.FlowTopologyGraph; +import io.kestra.core.models.triggers.AbstractTrigger; import io.kestra.core.models.validations.ManualConstraintViolation; import io.kestra.core.models.validations.ModelValidator; import io.kestra.core.models.validations.ValidateConstraintViolation; @@ -470,13 +471,21 @@ public List validateFlows( @Post(uri = "/validate/task", produces = MediaType.TEXT_JSON, consumes = MediaType.APPLICATION_YAML) @Operation(tags = {"Flows"}, summary = "Validate a list of flows") public ValidateConstraintViolation validateTask( - @Parameter(description = "A list of flows") @Body String task + @Parameter(description = "A list of flows") @Body String task, + @Parameter(description = "Type of task") @QueryValue(defaultValue = "task") String section ) { ValidateConstraintViolation.ValidateConstraintViolationBuilder validateConstraintViolationBuilder = ValidateConstraintViolation.builder(); try { - Task taskParse = yamlFlowParser.parse(task, Task.class); - modelValidator.validate(taskParse); + if (section.equals("task")) { + Task taskParse = yamlFlowParser.parse(task, Task.class); + modelValidator.validate(taskParse); + } else if (section.equals("trigger")) { + AbstractTrigger triggerParse = yamlFlowParser.parse(task, AbstractTrigger.class); + modelValidator.validate(triggerParse); + } else { + throw new IllegalArgumentException("Invalid section, must be 'task' or 'trigger'"); + } } catch (ConstraintViolationException e) { validateConstraintViolationBuilder.constraints(e.getMessage()); }