Repository: kestra-io/kestra (java)

Issue:
HTTP Trigger using method: HEAD fails
### Describe the issue

Creating an HTTP trigger with `method: HEAD` will fail to execute. The logs show it successfully making the http request but then failing with a NullPointerException.

```
[namespace: cp3] [flow: http-trigger-test] [trigger: http_check] [date: 2024-10-14T21:10:15.841Z] Trigger evaluation failed in the worker with error: null
```
```
java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Unknown Source)
at java.base/java.util.ImmutableCollections$MapN.<init>(Unknown Source)
at java.base/java.util.Map.of(Unknown Source)
at io.kestra.plugin.core.http.Trigger.evaluate(Trigger.java:157)
at io.kestra.core.runners.WorkerTriggerThread.doRun(WorkerTriggerThread.java:24)
at io.kestra.core.runners.AbstractWorkerThread.run(AbstractWorkerThread.java:57)
```

```yaml
id: http-trigger-test
namespace: cp3
triggers:
  - id: http_check
    type: io.kestra.plugin.core.http.Trigger
    uri: https://example.com
    method: HEAD
    interval: PT30S
tasks:
  - id: log-stats
    type: io.kestra.plugin.core.log.Log
    message: "Triggered flow!"
```


![](https://p.cp3.es/ss1728940248-2d5494.png)

### Environment

- Kestra Version: 0.19.2 - 4338437 Tue, Oct 8, 2024 9:52 AM
- Operating System (OS/Docker/Kubernetes): Docker (kestra/kestra:latest)
- Java Version (if you don't run kestra in Docker):

Pull request description:
HTTP Trigger using method: HEAD fails
- This PR fixes `NullPointerException` in `io.kestra.plugin.core.http.Trigger.evaluate` while running HTTP trigger using HEAD method.

### What changes are being made and why?

- Added small null check to prevent null value while creating `Map` if response body is empty.

Fixes #5459 

Patch:
diff --git a/core/src/main/java/io/kestra/core/models/flows/Input.java b/core/src/main/java/io/kestra/core/models/flows/Input.java
index 14d31187a..611df131b 100644
--- a/core/src/main/java/io/kestra/core/models/flows/Input.java
+++ b/core/src/main/java/io/kestra/core/models/flows/Input.java
@@ -41,7 +41,8 @@ import lombok.experimental.SuperBuilder;
     @JsonSubTypes.Type(value = TimeInput.class, name = "TIME"),
     @JsonSubTypes.Type(value = URIInput.class, name = "URI"),
     @JsonSubTypes.Type(value = MultiselectInput.class, name = "MULTISELECT"),
-    @JsonSubTypes.Type(value = YamlInput.class, name = "YAML")
+    @JsonSubTypes.Type(value = YamlInput.class, name = "YAML"),
+    @JsonSubTypes.Type(value = EmailInput.class, name = "EMAIL"),
 })
 @JsonInclude(JsonInclude.Include.NON_DEFAULT)
 public abstract class Input<T> implements Data {
diff --git a/core/src/main/java/io/kestra/core/models/flows/Type.java b/core/src/main/java/io/kestra/core/models/flows/Type.java
index e5a4e2735..4e8641525 100644
--- a/core/src/main/java/io/kestra/core/models/flows/Type.java
+++ b/core/src/main/java/io/kestra/core/models/flows/Type.java
@@ -24,7 +24,8 @@ public enum Type {
     SECRET(SecretInput.class.getName()),
     ARRAY(ArrayInput.class.getName()),
     MULTISELECT(MultiselectInput.class.getName()),
-    YAML(YamlInput.class.getName());
+    YAML(YamlInput.class.getName()),
+    EMAIL(EmailInput.class.getName());
 
     private final String clsName;
 
diff --git a/core/src/main/java/io/kestra/core/models/flows/input/EmailInput.java b/core/src/main/java/io/kestra/core/models/flows/input/EmailInput.java
new file mode 100644
index 000000000..bfb18e437
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/models/flows/input/EmailInput.java
@@ -0,0 +1,25 @@
+package io.kestra.core.models.flows.input;
+
+import io.kestra.core.models.flows.Input;
+import io.kestra.core.models.validations.ManualConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+
+import java.util.regex.Pattern;
+
+public class EmailInput extends Input<String> {
+
+    private static final String EMAIL_PATTERN = "^$|^[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$";
+
+    @Override
+    public void validate(String input) throws ConstraintViolationException {
+        if(!Pattern.matches(EMAIL_PATTERN, input)){
+            throw ManualConstraintViolation.toConstraintViolationException(
+                "The input must be a valid email",
+                this,
+                EmailInput.class,
+                getId(),
+                input
+            );
+        }
+    }
+}
diff --git a/core/src/main/java/io/kestra/core/runners/FlowInputOutput.java b/core/src/main/java/io/kestra/core/runners/FlowInputOutput.java
index b31b1f38b..05d7418bc 100644
--- a/core/src/main/java/io/kestra/core/runners/FlowInputOutput.java
+++ b/core/src/main/java/io/kestra/core/runners/FlowInputOutput.java
@@ -393,7 +393,7 @@ public class FlowInputOutput {
     private Object parseType(Execution execution, Type type, String id, Type elementType, Object current) throws Exception {
         try {
             return switch (type) {
-                case SELECT, ENUM, STRING -> current;
+                case SELECT, ENUM, STRING, EMAIL -> current;
                 case SECRET -> {
                     if (secretKey.isEmpty()) {
                         throw new Exception("Unable to use a `SECRET` input/output as encryption is not configured");
@@ -401,7 +401,8 @@ public class FlowInputOutput {
                     yield EncryptionService.encrypt(secretKey.get(), (String) current);
                 }
                 case INT -> current instanceof Integer ? current : Integer.valueOf((String) current);
-                case FLOAT -> current instanceof Float ? current : Float.valueOf((String) current);
+                // Assuming that after the render we must have a double/int, so we can safely use its toString representation
+                case FLOAT -> current instanceof Float ? current : Float.valueOf(current.toString());
                 case BOOLEAN -> current instanceof Boolean ? current : Boolean.valueOf((String) current);
                 case DATETIME -> Instant.parse(((String) current));
                 case DATE -> LocalDate.parse(((String) current));
diff --git a/core/src/main/java/io/kestra/plugin/core/http/Trigger.java b/core/src/main/java/io/kestra/plugin/core/http/Trigger.java
index c19907048..dc1222d84 100644
--- a/core/src/main/java/io/kestra/plugin/core/http/Trigger.java
+++ b/core/src/main/java/io/kestra/plugin/core/http/Trigger.java
@@ -16,6 +16,7 @@ import lombok.experimental.SuperBuilder;
 import org.slf4j.Logger;
 
 import java.time.Duration;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
 
@@ -67,7 +68,7 @@ import java.util.Optional;
                   - id: log_response
                     type: io.kestra.plugin.core.log.Log
                     message: '{{ trigger.body }}'
-                
+
                 triggers:
                   - id: http
                     type: io.kestra.plugin.core.http.Trigger
@@ -154,12 +155,12 @@ public class Trigger extends AbstractTrigger implements PollingTriggerInterface,
         Object body = this.encryptBody
             ? runContext.decrypt(output.getEncryptedBody().getValue())
             : output.getBody();
-        Map<String, Object> responseVariables = Map.of("response", Map.of(
-            "statusCode", output.getCode(),
-            "body", body,
-            "headers", output.getHeaders()
-            )
-        );
+
+        Map<String, Object> response = new HashMap<>();
+        response.put("statusCode", output.getCode());
+        response.put("body", body); // body can be null so we need a null-friendly map
+        response.put("headers", output.getHeaders());
+        Map<String, Object> responseVariables = Map.of("response", response);
         var renderedCondition = runContext.render(this.responseCondition, responseVariables);
         if (TruthUtils.isTruthy(renderedCondition)) {
             Execution execution = TriggerService.generateExecution(this, conditionContext, context, output);
diff --git a/ui/src/components/content/CardLogos.vue b/ui/src/components/content/CardLogos.vue
index dff5edfff..bcc488be7 100644
--- a/ui/src/components/content/CardLogos.vue
+++ b/ui/src/components/content/CardLogos.vue
@@ -6,7 +6,7 @@
                 width="222.67px"
                 height="125px"
                 loading="lazy"
-                :src="$store.getters['doc/resourceUrl']('/v1/docs/tutorial/logos/logo-dark-version.png')"
+                :src="$store.getters['doc/resourceUrl']('/docs/tutorial/logos/logo-dark-version.png')"
                 alt="Dark version logo"
             >
             <p class="title">
@@ -23,7 +23,7 @@
                 width="222.67px"
                 height="125px"
                 loading="lazy"
-                :src="$store.getters['doc/resourceUrl']('/v1/docs/tutorial/logos/logo-light-version.png')"
+                :src="$store.getters['doc/resourceUrl']('/docs/tutorial/logos/logo-light-version.png')"
                 alt="Light version logo"
             >
             <p class="title">
@@ -40,7 +40,7 @@
                 width="222.67px"
                 height="125px"
                 loading="lazy"
-                :src="$store.getters['doc/resourceUrl']('/v1/docs/tutorial/logos/logo-monogram-version.png')"
+                :src="$store.getters['doc/resourceUrl']('/docs/tutorial/logos/logo-monogram-version.png')"
                 alt="Monogram version logo"
             >
             <p class="title">
diff --git a/ui/src/components/docs/DocsLayout.vue b/ui/src/components/docs/DocsLayout.vue
index dc164e672..f5e1fec1c 100644
--- a/ui/src/components/docs/DocsLayout.vue
+++ b/ui/src/components/docs/DocsLayout.vue
@@ -48,16 +48,16 @@
         overflow-y: auto;
 
         &::-webkit-scrollbar {
-            width: 2px;
+            width: 8px;
         }
 
         &::-webkit-scrollbar-track {
-            -webkit-border-radius: 10px;
+            background: var(--card-bg);
         }
 
         &::-webkit-scrollbar-thumb {
-            -webkit-border-radius: 10px;
-            background: var(--bs-gray-600);
+            background: var(--bs-primary);
+            border-radius: 0px;
         }
 
         &.collapsed {
diff --git a/ui/src/components/executions/ForEachStatus.vue b/ui/src/components/executions/ForEachStatus.vue
index 1b321495f..d855eeaa3 100644
--- a/ui/src/components/executions/ForEachStatus.vue
+++ b/ui/src/components/executions/ForEachStatus.vue
@@ -19,7 +19,7 @@
             </router-link>
             <div v-for="state in State.allStates()" :key="state.key">
                 <router-link :to="goToExecutionsList(state.key)" class="el-button count-button" v-if="localSubflowStatus[state.key] >= 0">
-                    {{ capitalizeFirstLetter(state.key) }}
+                    {{ capitalizeFirstLetter(getStateToBeDisplayed(state.key)) }}
                     <span class="counter">{{ localSubflowStatus[state.key] }}</span>
                     <div class="dot rounded-5" :class="`bg-${state.colorClass}`" />
                 </router-link>
@@ -29,6 +29,7 @@
 </template>
 <script>
     import {cssVariable} from "@kestra-io/ui-libs/src/utils/global";
+    import {stateDisplayValues} from "../../utils/constants";
     import State from "../../utils/state";
     import throttle from "lodash/throttle"
 
@@ -79,6 +80,13 @@
             capitalizeFirstLetter(str) {
                 return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
             },
+            getStateToBeDisplayed(str){
+                if(str === State.RUNNING){
+                    return stateDisplayValues.INPROGRESS;
+                }else{
+                    return str;
+                }
+            },
             goToExecutionsList(state) {
                 const queries = {
                     triggerExecutionId: this.executionId,
diff --git a/ui/src/components/executions/Overview.vue b/ui/src/components/executions/Overview.vue
index d607a4bab..e9236c744 100644
--- a/ui/src/components/executions/Overview.vue
+++ b/ui/src/components/executions/Overview.vue
@@ -1,5 +1,5 @@
 <template>
-    <div v-if="execution">
+    <div v-if="execution" class="execution-overview">
         <el-row class="mb-3">
             <el-col :span="12" class="crud-align">
                 <crud type="CREATE" permission="EXECUTION" :detail="{executionId: execution.id}" />
@@ -47,31 +47,30 @@
             </el-table-column>
         </el-table>
 
-        <div v-if="execution.trigger" class="mt-4">
+        <div v-if="execution.trigger" class="my-5">
             <h5>{{ $t("trigger") }}</h5>
-            <vars :execution="execution" :data="triggerVariables" />
+            <KestraCascader :options="transform({...execution.trigger, ...(execution.trigger.trigger ? execution.trigger.trigger : {})})" class="overflow-auto" />
         </div>
 
-        <div v-if="execution.inputs" class="mt-4">
+        <div v-if="execution.inputs" class="my-5">
             <h5>{{ $t("inputs") }}</h5>
-            <vars :execution="execution" :data="inputs" key-label-translation-key="id" />
+            <KestraCascader :options="transform(execution.inputs)" class="overflow-auto" />
         </div>
 
-        <div v-if="execution.variables" class="mt-4">
+        <div v-if="execution.variables" class="my-5">
             <h5>{{ $t("variables") }}</h5>
-            <vars :execution="execution" :data="execution.variables" />
+            <KestraCascader :options="transform(execution.variables)" class="overflow-auto" />
         </div>
 
-        <div v-if="execution.outputs" class="mt-4">
+        <div v-if="execution.outputs" class="my-5">
             <h5>{{ $t("outputs") }}</h5>
-            <vars :execution="execution" :data="execution.outputs" />
+            <KestraCascader :options="transform(execution.outputs)" class="overflow-auto" />
         </div>
     </div>
 </template>
 <script>
     import {mapState} from "vuex";
     import Status from "../Status.vue";
-    import Vars from "./Vars.vue";
     import SetLabels from "./SetLabels.vue";
     import Restart from "./Restart.vue";
     import Resume from "./Resume.vue";
@@ -83,6 +82,7 @@
     import Labels from "../layout/Labels.vue"
     import {toRaw} from "vue";
     import ChangeExecutionStatus from "./ChangeExecutionStatus.vue";
+    import KestraCascader from "../../components/kestra/Cascader.vue"
 
     export default {
         components: {
@@ -91,15 +91,41 @@
             Status,
             SetLabels,
             Restart,
-            Vars,
             Resume,
             Kill,
             DateAgo,
             Labels,
-            Crud
+            Crud,
+            KestraCascader
         },
         emits: ["follow"],
         methods: {
+            transform(obj) {
+                return Object.entries(obj).map(([key, value]) => {
+                    const children =
+                        typeof value === "object" && value !== null
+                            ? Object.entries(value).map(
+                                ([k, v]) => this.transform({[k]: v})[0],
+                            )
+                            : [{label: value, value: value}];
+
+                    // Filter out children with undefined label and value
+                    const filteredChildren = children.filter(
+                        (child) =>
+                            child.label !== undefined || child.value !== undefined,
+                    );
+
+                    // Return node with or without children based on existence
+                    const node = {label: key, value: key};
+
+                    // Include children only if there are valid entries
+                    if (filteredChildren.length) {
+                        node.children = filteredChildren;
+                    }
+
+                    return node;
+                });
+            },
             forwardEvent(type, event) {
                 this.$emit(type, event);
             },
@@ -187,21 +213,84 @@
                     })
                 })
                 return inputs;
-            },
-            // This is used to display correctly trigger variables
-            triggerVariables() {
-                let trigger = this.execution.trigger
-                trigger["trigger"] = this.execution.trigger.variables
-                delete trigger["variables"]
-
-                return trigger
             }
         },
     };
 </script>
-<style scoped lang="scss">
-    .crud-align {
-        display: flex;
-        align-items: center;
+
+<style lang="scss">
+.crud-align {
+    display: flex;
+    align-items: center;
+}
+
+.execution-overview {
+    .cascader {
+        &::-webkit-scrollbar {
+            height: 5px;
+        }
+
+        &::-webkit-scrollbar-track {
+            background: var(--card-bg);
+        }
+
+        &::-webkit-scrollbar-thumb {
+            background: var(--bs-primary);
+            border-radius: 0px;
+        }
+    }
+
+    .wrapper {
+        background: var(--card-bg);
+    }
+
+    .el-cascader-menu {
+        min-width: 300px;
+        max-width: 300px;
+
+        .el-cascader-menu__list {
+            padding: 0;
+        }
+
+        .el-cascader-menu__wrap {
+            height: 100%;
+        }
+
+        & .el-cascader-node {
+            height: 36px;
+            line-height: 36px;
+            font-size: var(--el-font-size-small);
+            color: var(--el-text-color-regular);
+            padding: 0 30px 0 5px;
+
+            &[aria-haspopup="false"] {
+                padding-right: 0.5rem !important;
+            }
+
+            &:hover {
+                background-color: var(--bs-border-color);
+            }
+
+            &.in-active-path,
+            &.is-active {
+                background-color: var(--bs-border-color);
+                font-weight: normal;
+            }
+
+            .el-cascader-node__prefix {
+                display: none;
+            }
+
+            .task .wrapper {
+                align-self: center;
+                height: var(--el-font-size-small);
+                width: var(--el-font-size-small);
+            }
+
+            code span.regular {
+                color: var(--el-text-color-regular);
+            }
+        }
     }
-</style>
+}
+</style>
\ No newline at end of file
diff --git a/ui/src/components/executions/outputs/Wrapper.vue b/ui/src/components/executions/outputs/Wrapper.vue
index d7d5f4728..50667048a 100644
--- a/ui/src/components/executions/outputs/Wrapper.vue
+++ b/ui/src/components/executions/outputs/Wrapper.vue
@@ -291,6 +291,24 @@
 
 <style lang="scss">
 .outputs {
+    .el-scrollbar.el-cascader-menu:nth-of-type(-n + 2) ul li:first-child,
+    .values {
+        pointer-events: none;
+        margin: 0.75rem 0 1.25rem 0;
+    }
+
+    .debug {
+        background: var(--bs-gray-100);
+    }
+
+    .bordered {
+        border: 1px solid var(--bs-border-color);
+    }
+
+    .bordered > .el-collapse-item {
+        margin-bottom: 0px !important;
+    }
+
     .cascader {
         &::-webkit-scrollbar {
             height: 5px;
@@ -309,73 +327,53 @@
     .wrapper {
         background: var(--card-bg);
     }
-}
-
-.el-cascader-menu {
-    min-width: 300px;
-    max-width: 300px;
-
-    &:last-child {
-        border-right: 1px solid var(--bs-border-color);
-    }
-
-    .el-cascader-menu__wrap {
-        height: 100%;
-    }
-
-    & .el-cascader-node {
-        height: 36px;
-        line-height: 36px;
-        font-size: var(--el-font-size-small);
-        color: var(--el-text-color-regular);
-
-        &[aria-haspopup="false"] {
-            padding-right: 0.5rem !important;
-        }
-
-        &:hover {
-            background-color: var(--bs-border-color);
-        }
 
-        &.in-active-path,
-        &.is-active {
-            background-color: var(--bs-border-color);
-            font-weight: normal;
-        }
+    .el-cascader-menu {
+        min-width: 300px;
+        max-width: 300px;
 
-        .el-cascader-node__prefix {
-            display: none;
+        &:last-child {
+            border-right: 1px solid var(--bs-border-color);
         }
 
-        .task .wrapper {
-            align-self: center;
-            height: var(--el-font-size-small);
-            width: var(--el-font-size-small);
+        .el-cascader-menu__wrap {
+            height: 100%;
         }
 
-        code span.regular {
+        & .el-cascader-node {
+            height: 36px;
+            line-height: 36px;
+            font-size: var(--el-font-size-small);
             color: var(--el-text-color-regular);
-        }
-    }
-}
 
+            &[aria-haspopup="false"] {
+                padding-right: 0.5rem !important;
+            }
 
-.el-scrollbar.el-cascader-menu:nth-of-type(-n+2) ul li:first-child,
-.values {
-    pointer-events: none;
-    margin: 0.75rem 0 1.25rem 0;
-}
+            &:hover {
+                background-color: var(--bs-border-color);
+            }
 
-.debug {
-    background: var(--bs-gray-100);
-}
+            &.in-active-path,
+            &.is-active {
+                background-color: var(--bs-border-color);
+                font-weight: normal;
+            }
 
-.bordered {
-    border: 1px solid var(--bs-border-color)
-}
+            .el-cascader-node__prefix {
+                display: none;
+            }
 
-.bordered > .el-collapse-item{
-    margin-bottom :0px !important
-}
+            .task .wrapper {
+                align-self: center;
+                height: var(--el-font-size-small);
+                width: var(--el-font-size-small);
+            }
 
+            code span.regular {
+                color: var(--el-text-color-regular);
+            }
+        }
+    }
+}
 </style>
\ No newline at end of file
diff --git a/ui/src/components/flows/tasks/TaskBasic.vue b/ui/src/components/flows/tasks/TaskBasic.vue
index 974f9d080..d4f54ec63 100644
--- a/ui/src/components/flows/tasks/TaskBasic.vue
+++ b/ui/src/components/flows/tasks/TaskBasic.vue
@@ -30,6 +30,7 @@
             :schema="schema"
             :required="isRequired(key)"
             :definitions="definitions"
+            :min="getExclusiveMinimum(key)"
         />
     </el-form-item>
 </template>
@@ -59,7 +60,6 @@
                     const properties = this.schema.properties
                     return this.sortProperties(properties)
                 }
-
                 return undefined;
             }
         },
@@ -123,6 +123,11 @@
                     (schema.description ? schema.description : "")
                 );
             },
+            getExclusiveMinimum(key) {
+                const property = this.schema.properties[key];
+                const propertyHasExclusiveMinimum = property && property.exclusiveMinimum
+                return propertyHasExclusiveMinimum ? property.exclusiveMinimum : null;
+            }
         },
     };
 </script>
diff --git a/ui/src/components/inputs/InputsForm.vue b/ui/src/components/inputs/InputsForm.vue
index 8c9078c39..c6acb15a0 100644
--- a/ui/src/components/inputs/InputsForm.vue
+++ b/ui/src/components/inputs/InputsForm.vue
@@ -11,7 +11,7 @@
                 :full-height="false"
                 :input="true"
                 :navbar="false"
-                v-if="input.type === 'STRING' || input.type === 'URI'"
+                v-if="input.type === 'STRING' || input.type === 'URI' || input.type === 'EMAIL'"
                 v-model="inputs[input.id]"
                 @update:model-value="onChange"
                 @confirm="onSubmit"
diff --git a/ui/src/components/kestra/Cascader.vue b/ui/src/components/kestra/Cascader.vue
new file mode 100644
index 000000000..6faf17043
--- /dev/null
+++ b/ui/src/components/kestra/Cascader.vue
@@ -0,0 +1,20 @@
+<template>
+    <el-cascader-panel :options="options" />
+</template>
+
+<script setup lang="ts">
+    import type {PropType} from "vue";
+
+    interface Options {
+        label: string;
+        value: [string, number, boolean];
+        children?: Options[];
+    }
+
+    defineProps({
+        options: {
+            type: Object as PropType<Options>,
+            required: true,
+        },
+    });
+</script>
diff --git a/ui/src/components/plugins/Toc.vue b/ui/src/components/plugins/Toc.vue
index eaf8b24c7..86b5c1cb4 100644
--- a/ui/src/components/plugins/Toc.vue
+++ b/ui/src/components/plugins/Toc.vue
@@ -7,26 +7,24 @@
             clearable
         />
         <el-collapse accordion v-model="activeNames">
-            <template
-                :key="plugin.title"
-                v-for="(plugin) in sortedPlugins(pluginsList)"
-            >
+            <template :key="plugin.title" v-for="(plugin) in sortedPlugins(pluginsList)">
                 <el-collapse-item
                     v-if="isVisible(plugin)"
                     :name="plugin.group"
                     :title="plugin.title.capitalize()"
                     :key="plugin.group"
+                    :ref="`plugin-${plugin.group}`"
                 >
                     <ul class="toc-h3">
                         <li v-for="(types, namespace) in group(plugin, plugin.tasks)" :key="namespace">
                             <h6>{{ namespace }}</h6>
                             <ul class="toc-h4">
-                                <li v-for="(classes, type) in types" :key="type+'-'+ namespace">
+                                <li v-for="(classes, type) in types" :key="type + '-' + namespace">
                                     <h6>{{ $filters.cap(type) }}</h6>
                                     <ul class="section-nav toc-h5">
                                         <li v-for="cls in classes" :key="cls">
                                             <router-link
-                                                @click="$emit('routerChange')"
+                                                @click="$emit('routerChange'); handlePluginChange(namespace)"
                                                 :to="{name: 'plugins/view', params: {cls: namespace + '.' + cls}}"
                                             >
                                                 <div class="icon">
@@ -38,7 +36,8 @@
                                                 </div>
                                                 <span
                                                     :class="$route.params.cls === (namespace + '.' + cls) ? 'selected mx-2' : 'mx-2'"
-                                                >{{ cls }}</span>
+                                                >{{
+                                                    cls }}</span>
                                             </router-link>
                                         </li>
                                     </ul>
@@ -66,11 +65,14 @@
             }
         },
         mounted() {
+
             this.plugins.forEach(plugin => {
                 if (plugin.tasks.includes(this.$route.params.cls)) {
                     this.activeNames = [plugin.group]
+                    localStorage.setItem("activePlugin", plugin.group);
                 }
             })
+            this.scrollToActivePlugin();
         },
         components: {
             TaskIcon
@@ -117,6 +119,26 @@
             }
         },
         methods: {
+
+            scrollToActivePlugin() {
+                const activePlugin = localStorage.getItem("activePlugin");
+                if (activePlugin) {
+                    // Use Vue's $refs to scroll to the specific plugin group
+                    this.$nextTick(() => {
+                        const pluginElement = this.$refs[`plugin-${activePlugin}`];
+                        if (pluginElement && pluginElement[0]) {
+                            pluginElement[0].$el.scrollIntoView({behavior: "smooth", block: "start"});
+                        }
+                    });
+                }
+            },
+
+            // When user navigates to a different plugin, save the new plugin group to localStorage
+            handlePluginChange(pluginGroup) {
+                this.activeNames = [pluginGroup];
+                localStorage.setItem("activePlugin", pluginGroup); // Save to localStorage
+            },
+
             sortedPlugins(plugins) {
                 return plugins
                     .sort((a, b) => {
@@ -158,70 +180,71 @@
 </script>
 
 <style lang="scss">
-    .plugins-list {
-        &.enhance-readability {
-            padding: calc(var(--spacer) * 1.5);
-            background-color: var(--bs-gray-100);
-        }
+.plugins-list {
+    &.enhance-readability {
+        padding: calc(var(--spacer) * 1.5);
+        background-color: var(--bs-gray-100);
+    }
 
-        &::-webkit-scrollbar {
-            width: 2px;
-        }
+    &::-webkit-scrollbar {
+        width: 2px;
+    }
 
-        &::-webkit-scrollbar-track {
-            -webkit-border-radius: 10px;
-        }
+    &::-webkit-scrollbar-track {
+        -webkit-border-radius: 10px;
+    }
 
-        &::-webkit-scrollbar-thumb {
-            -webkit-border-radius: 10px;
-            background: var(--bs-gray-600);
-        }
+    &::-webkit-scrollbar-thumb {
+        -webkit-border-radius: 10px;
+        background: var(--bs-gray-600);
+    }
 
-        .el-collapse-item__header {
-            font-size: 0.875rem;
-        }
+    .el-collapse-item__header {
+        font-size: 0.875rem;
+    }
+
+    ul {
+        list-style: none;
+        padding-inline-start: 0;
+        margin-bottom: 0;
+        font-size: var(--font-size-xs);
+        margin-left: calc(var(--spacer) / 2);
+    }
+
+    h6,
+    a {
+        word-break: break-all;
+        color: var(--el-collapse-header-text-color);
+    }
 
-        ul {
-            list-style: none;
-            padding-inline-start: 0;
-            margin-bottom: 0;
-            font-size: var(--font-size-xs);
-            margin-left: calc(var(--spacer) / 2);
+    .toc-h3 {
+        .icon {
+            width: var(--font-size-sm);
+            height: var(--font-size-sm);
+            display: inline-block;
+            position: relative;
         }
 
-        h6, a {
-            word-break: break-all;
-            color: var(--el-collapse-header-text-color);
+        h6 {
+            font-size: 1.1em;
         }
 
-        .toc-h3 {
-            .icon {
-                width: var(--font-size-sm);
-                height: var(--font-size-sm);
-                display: inline-block;
-                position: relative;
-            }
+        .toc-h4 {
+            margin-left: var(--spacer);
 
             h6 {
-                font-size: 1.1em;
+                font-size: var(--font-size-sm);
+                margin-bottom: calc(var(--spacer) / 2);
             }
 
-            .toc-h4 {
-                margin-left: var(--spacer);
-
-                h6 {
-                    font-size: var(--font-size-sm);
-                    margin-bottom: calc(var(--spacer) / 2);
-                }
-
-                li {
-                    margin-bottom: calc(var(--spacer) / 2);
-                }
+            li {
+                margin-bottom: calc(var(--spacer) / 2);
             }
         }
     }
+}
 
-    .selected {
-        color: var(--bs-purple);
-    }
+.selected {
+    color: var(--bs-purple);
+}
 </style>
diff --git a/ui/src/utils/constants.js b/ui/src/utils/constants.js
index 217ab86ef..7bbd1dfbf 100644
--- a/ui/src/utils/constants.js
+++ b/ui/src/utils/constants.js
@@ -47,3 +47,7 @@ export const executeFlowBehaviours = {
     NEW_TAB: "new tab"
 }
 
+export const stateDisplayValues = {
+    INPROGRESS: "IN-PROGRESS"
+}
+


Test patch:
diff --git a/webserver/src/test/java/io/kestra/webserver/controllers/api/PluginControllerTest.java b/webserver/src/test/java/io/kestra/webserver/controllers/api/PluginControllerTest.java
index 8ec1e1ced..6878c1ce2 100644
--- a/webserver/src/test/java/io/kestra/webserver/controllers/api/PluginControllerTest.java
+++ b/webserver/src/test/java/io/kestra/webserver/controllers/api/PluginControllerTest.java
@@ -213,7 +213,7 @@ class PluginControllerTest {
                 Argument.listOf(InputType.class)
             );
 
-            assertThat(doc.size(), is(17));
+            assertThat(doc.size(), is(18));
         });
     }