Repository: kestra-io/kestra
Issue: Allow to pass execution labels via web UI
Labels: frontend

### Feature description

Users should be able to pass execution labels through the web UI. The labels might be passed via the existing `TriggerFlow` modal.
```[tasklist]
### Tasks
```


Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/ui/src/components/flows/FlowRun.vue b/ui/src/components/flows/FlowRun.vue
index ea2584bf03..ff230ed36a 100644
--- a/ui/src/components/flows/FlowRun.vue
+++ b/ui/src/components/flows/FlowRun.vue
@@ -4,7 +4,8 @@
             <strong>{{ $t('disabled flow title') }}</strong><br>
             {{ $t('disabled flow desc') }}
         </el-alert>
-        <el-form label-position="top" :model="inputs" ref="form">
+
+        <el-form label-position="top" :model="inputs" ref="form" @submit.prevent>
             <el-form-item
                 v-for="input in flow.inputs || []"
                 :key="input.id"
@@ -60,8 +61,10 @@
                             autocomplete="off"
                             :style="{display: typeof(inputs[input.name]) === 'string' && inputs[input.name].startsWith('kestra:///') ? 'none': ''}"
                         >
-                        <label v-if="typeof(inputs[input.name]) === 'string' && inputs[input.name].startsWith('kestra:///')"
-                               :for="input.name+'-file'">Kestra Internal Storage File</label>
+                        <label
+                            v-if="typeof(inputs[input.name]) === 'string' && inputs[input.name].startsWith('kestra:///')"
+                            :for="input.name+'-file'"
+                        >Kestra Internal Storage File</label>
                     </div>
                 </div>
                 <editor
@@ -75,6 +78,13 @@
 
                 <small v-if="input.description" class="text-muted">{{ input.description }}</small>
             </el-form-item>
+            <el-form-item
+                :label="$t('execution labels')"
+            >
+                <label-filter
+                    v-model:model-value="executionLabels"
+                />
+            </el-form-item>
             <div class="bottom-buttons">
                 <div class="left-align">
                     <el-form-item>
@@ -85,7 +95,7 @@
                 </div>
                 <div class="right-align">
                     <el-form-item class="submit">
-                        <el-button :icon="Flash" class="flow-run-trigger-button" @click="onSubmit($refs.form)" type="primary" :disabled="flow.disabled">
+                        <el-button :icon="Flash" class="flow-run-trigger-button" @click="onSubmit($refs.form)" type="primary" :disabled="flow.disabled || haveBadLabels">
                             {{ $t('launch execution') }}
                         </el-button>
                     </el-form-item>
@@ -104,10 +114,11 @@
     import {mapState} from "vuex";
     import {executeTask} from "../../utils/submitTask"
     import Editor from "../../components/inputs/Editor.vue";
+    import LabelFilter from "../../components/labels/LabelFilter.vue";
     import {pageFromRoute} from "../../utils/eventsRouter";
 
     export default {
-        components: {Editor},
+        components: {Editor, LabelFilter},
         props: {
             redirect: {
                 type: Boolean,
@@ -116,7 +127,10 @@
         },
         data() {
             return {
-                inputs: {}
+                inputs: {},
+                inputNewLabel: "",
+                executionLabels: [],
+                inputVisible: false
             };
         },
         emits: ["executionTrigger"],
@@ -153,8 +167,14 @@
             ...mapState("flow", ["flow"]),
             ...mapState("core", ["guidedProperties"]),
             ...mapState("execution", ["execution"]),
+            haveBadLabels() {
+                return this.executionLabels.some(label => label.split(":").length !== 2)
+            }
         },
         methods: {
+            isBadLabel(tag) {
+                return tag.split(":").length !== 2
+            },
             fillInputsFromExecution(){
                 const nonEmptyInputNames = Object.keys(this.execution.inputs);
                 this.inputs = Object.fromEntries(
@@ -163,11 +183,11 @@
                             const inputName = input.name;
                             const inputType = input.type;
                             let inputValue = this.execution.inputs[inputName];
-                            if (inputType === 'DATE' || inputType === 'DATETIME') {
+                            if (inputType === "DATE" || inputType === "DATETIME") {
                                 inputValue = this.$moment(inputValue).toISOString()
-                            }else if (inputType === 'DURATION' || inputType === 'TIME') {
+                            }else if (inputType === "DURATION" || inputType === "TIME") {
                                 inputValue = this.$moment().startOf("day").add(inputValue, "seconds").toString()
-                            }else if (inputType === 'JSON') {
+                            }else if (inputType === "JSON") {
                                 inputValue = JSON.stringify(inputValue).toString()
                             }
 
@@ -188,7 +208,8 @@
                         executeTask(this, this.flow, this.inputs, {
                             redirect: this.redirect,
                             id: this.flow.id,
-                            namespace: this.flow.namespace
+                            namespace: this.flow.namespace,
+                            labels: this.executionLabels
                         })
                         this.$emit("executionTrigger");
                     });
@@ -250,6 +271,19 @@
                 }
 
                 return true;
+            },
+            handleClose(label) {
+                this.executionLabels.splice(this.executionLabels.indexOf(label), 1)
+            },
+            showInput() {
+                this.inputVisible = true;
+            },
+            handleInputConfirm() {
+                if (this.inputNewLabel) {
+                    this.executionLabels.push(this.inputNewLabel)
+                }
+                this.inputVisible = false
+                this.inputNewLabel = ""
             }
         },
         watch: {
@@ -285,5 +319,6 @@
     .right-align :deep(div) {
         flex-direction: row-reverse;
     }
+
 }
 </style>
\ No newline at end of file
diff --git a/ui/src/components/flows/TriggerFlow.vue b/ui/src/components/flows/TriggerFlow.vue
index 5f65ab7369..f9bc660afc 100644
--- a/ui/src/components/flows/TriggerFlow.vue
+++ b/ui/src/components/flows/TriggerFlow.vue
@@ -16,11 +16,9 @@
 <script>
     import FlowRun from "./FlowRun.vue";
     import {mapState} from "vuex";
-    import {executeTask} from "../../utils/submitTask"
     import Flash from "vue-material-design-icons/Flash.vue";
     import {shallowRef} from "vue";
     import {pageFromRoute} from "../../utils/eventsRouter";
-    import action from "../../models/action";
 
     export default {
         components: {
@@ -76,20 +74,7 @@
                     this.$tours["guidedTour"].nextStep();
                     return;
                 }
-
-                if (!this.flow.inputs || this.flow.inputs.length === 0) {
-                    this.$toast().confirm(
-                        this.$t("execute flow now ?"),
-                        () => executeTask(this, this.flow, {}, {
-                            id: this.flowId,
-                            namespace: this.namespace,
-                            redirect: true
-                        }),
-                        () => this.isOpen = false
-                    )
-                } else {
-                    this.isOpen = !this.isOpen
-                }
+                this.isOpen = !this.isOpen
             },
 
             closeModal() {
diff --git a/ui/src/components/labels/LabelFilter.vue b/ui/src/components/labels/LabelFilter.vue
index 14bbfa2128..afea33abad 100644
--- a/ui/src/components/labels/LabelFilter.vue
+++ b/ui/src/components/labels/LabelFilter.vue
@@ -6,7 +6,7 @@
         filterable
         allow-create
         clearable
-        collapse-tags
+        :collapse-tags="collapseTags"
         default-first-option
         :persistent="false"
         :reserve-keyword="false"
diff --git a/ui/src/stores/executions.js b/ui/src/stores/executions.js
index 2b24d2f923..a293f9b544 100644
--- a/ui/src/stores/executions.js
+++ b/ui/src/stores/executions.js
@@ -94,6 +94,9 @@ export default {
                 timeout: 60 * 60 * 1000,
                 headers: {
                     "content-type": "multipart/form-data"
+                },
+                params: {
+                    labels: options.labels ?? []
                 }
             })
         },
diff --git a/ui/src/translations.json b/ui/src/translations.json
index e83c85b869..a96739f6fd 100644
--- a/ui/src/translations.json
+++ b/ui/src/translations.json
@@ -260,6 +260,7 @@
     "label": "Label",
     "labels": "Labels",
     "label filter placeholder": "Label as 'key:value'",
+    "execution labels": "Execution labels",
     "feeds": {
       "title": "What's new at Kestra"
     },
@@ -711,6 +712,7 @@
     "label": "Label",
     "labels": "Labels",
     "label filter placeholder": "Label as 'key:value'",
+    "execution labels": "Labels d'exécution",
     "feeds": {
       "title": "Quoi de neuf sur Kestra"
     },


Test patch: