Repository: kestra-io/kestra
Issue: [UI bug] Administration trigger set to disabled in the source code should not be toggle-able (the toggle should be greyed out/locked)
Labels: bug

### Issue description

I noticed that there seems to be no correlation between enabling or disabling triggers in the UI via Administration -> Triggers -> Enable/Disable toggle and the flow definition, where triggers can have a `disabled: true` flag which is also described in the [docs](https://kestra.io/docs/workflow-components/disabled#disabled-trigger).

Use case is to sync flows from Git, with triggers. Those triggers would be disabled by default (in the flow definition yaml), but perhaps can be turned on from the Administration Triggers UI.

Maybe it's still a new feature and being worked out. :)

Running on 0.15.0-SNAPSHOT

Issue comments:
The toggle is independent of what is configured in the code. If you disable the trigger explicitly in your flow code, there is no other way to reenable it unless you change the source code again.

If `disabled: true` in the source code, you should not be able to toggle it on. It works on the Flow's Trigger page. However, you are right, this is indeed a bug on the Administration page: https://share.descript.com/view/ZUaJMVu2JJt

Repro: try any disabled trigger:

```yaml
id: hello-world
namespace: company.team
tasks:
  - id: hello
    type: io.kestra.core.tasks.log.Log
    message: Kestra team wishes you a great day! 👋

triggers:
  - id: schedule
    type: io.kestra.core.models.triggers.types.Schedule
    disabled: true
    cron: "* * * * *"
```

Then go to the Admin Trigger page and you will be able to toggle it, even though you shouldn't as it has no effect (explicitly disabled in code takes priority). 

![image](https://github.com/kestra-io/kestra/assets/86264395/9dd19a9a-2203-4449-bfeb-70146e42d6df)


Pull request review comments:

Patch that resolved the issue:
diff --git a/ui/src/components/admin/Triggers.vue b/ui/src/components/admin/Triggers.vue
index 82ba13e6bf..6a1c513757 100644
--- a/ui/src/components/admin/Triggers.vue
+++ b/ui/src/components/admin/Triggers.vue
@@ -24,7 +24,7 @@
                 </template>
                 <template #table>
                     <el-table
-                        :data="triggers"
+                        :data="triggersMerged"
                         ref="table"
                         :default-sort="{prop: 'flowId', order: 'ascending'}"
                         stripe
@@ -199,7 +199,9 @@
                 }).then(triggersData => {
                     this.triggers = triggersData.results;
                     this.total = triggersData.total;
-                    callback();
+                    if (callback) {
+                        callback();
+                    }
                 });
             },
             async unlock() {
@@ -225,6 +227,15 @@
                 this.triggerToUnlock = undefined;
             },
             setDisabled(trigger, value) {
+                if (trigger.codeDisabled) {
+                    this.$message({
+                        message: this.$t("triggerflow disabled"),
+                        type: "error",
+                        showClose: true,
+                        duration: 1500
+                    });
+                    return;
+                }
                 this.$store.dispatch("trigger/update", {...trigger, disabled: !value})
                     .then(_ => {
                         this.loadData();
@@ -237,6 +248,11 @@
                 return {
                     title: this.$t("triggers")
                 }
+            },
+            triggersMerged() {
+                return this.triggers.map(triggers => {
+                    return {...triggers.abstractTrigger, ...triggers.triggerContext, codeDisabled: triggers.abstractTrigger.disabled}
+                })
             }
         }
     };
diff --git a/ui/src/translations.json b/ui/src/translations.json
index 952edaec92..31cad847d5 100644
--- a/ui/src/translations.json
+++ b/ui/src/translations.json
@@ -269,6 +269,7 @@
     "trigger": "Trigger",
     "triggers": "Triggers",
     "trigger details": "Trigger details",
+    "triggerflow disabled": "Trigger is disabled from flow definition",
     "conditions": "conditions",
     "taskruns": "Task Runs",
     "Fold auto": "Editor: automatic fold of multilines",
@@ -865,6 +866,7 @@
     "trigger": "Déclencheur",
     "triggers": "Déclencheurs",
     "trigger details": "Détails du déclencheur",
+    "triggerflow disabled": "Déclencheur désactivé depuis la définition du flow",
     "conditions": "conditions",
     "taskruns": "Execution de tâches",
     "Fold auto": "Éditeur : Replier automatiquement le contenu multi-lignes",
diff --git a/webserver/src/main/java/io/kestra/webserver/controllers/TriggerController.java b/webserver/src/main/java/io/kestra/webserver/controllers/TriggerController.java
index d195c8fbba..730ee01c11 100644
--- a/webserver/src/main/java/io/kestra/webserver/controllers/TriggerController.java
+++ b/webserver/src/main/java/io/kestra/webserver/controllers/TriggerController.java
@@ -7,6 +7,7 @@
 import io.kestra.core.models.triggers.Trigger;
 import io.kestra.core.models.triggers.TriggerContext;
 import io.kestra.core.queues.QueueInterface;
+import io.kestra.core.repositories.ArrayListTotal;
 import io.kestra.core.repositories.FlowRepositoryInterface;
 import io.kestra.core.repositories.TriggerRepositoryInterface;
 import io.kestra.core.runners.RunContext;
@@ -25,12 +26,17 @@
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import jakarta.inject.Inject;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
 
 import java.time.ZonedDateTime;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 
 @Controller("/api/v1/triggers")
+@Slf4j
 public class TriggerController {
     @Inject
     private TriggerRepositoryInterface triggerRepository;
@@ -45,28 +51,57 @@ public class TriggerController {
     private TenantService tenantService;
 
     @Inject
-    private  RunContextFactory runContextFactory;
+    private RunContextFactory runContextFactory;
 
     @Inject
-    private  ConditionService conditionService;
+    private ConditionService conditionService;
 
     @ExecuteOn(TaskExecutors.IO)
     @Get(uri = "/search")
     @Operation(tags = {"Triggers"}, summary = "Search for triggers")
-    public PagedResults<Trigger> search(
+    public PagedResults<Triggers> search(
         @Parameter(description = "The current page") @QueryValue(defaultValue = "1") int page,
         @Parameter(description = "The current page size") @QueryValue(defaultValue = "10") int size,
         @Parameter(description = "The sort of current page") @Nullable @QueryValue List<String> sort,
         @Parameter(description = "A string filter") @Nullable @QueryValue(value = "q") String query,
         @Parameter(description = "A namespace filter prefix") @Nullable @QueryValue String namespace
     ) throws HttpStatusException {
-        return PagedResults.of(triggerRepository.find(
+
+        ArrayListTotal<Trigger> triggerContexts = triggerRepository.find(
             PageableUtils.from(page, size, sort, triggerRepository.sortMapping()),
             query,
             tenantService.resolveTenant(),
             namespace,
             null
-        ));
+        );
+
+        List<Triggers> triggers = new ArrayList<>();
+        triggerContexts.forEach(tc -> {
+            Optional<Flow> flow = flowRepository.findById(tc.getTenantId(), tc.getNamespace(), tc.getFlowId());
+            if (flow.isEmpty()) {
+                // Warn instead of throwing to avoid blocking the trigger UI
+                log.warn(String.format("Flow %s not found for trigger %s", tc.getFlowId(), tc.getTriggerId()));
+            }
+
+            AbstractTrigger abstractTrigger = flow.get().getTriggers().stream().filter(t -> t.getId().equals(tc.getTriggerId())).findFirst().orElse(null);
+            if (abstractTrigger == null) {
+                // Warn instead of throwing to avoid blocking the trigger UI
+                log.warn(String.format("Flow %s has no trigger %s", tc.getFlowId(), tc.getTriggerId()));
+            }
+
+            triggers.add(Triggers.builder()
+                .abstractTrigger(abstractTrigger)
+                .triggerContext(tc)
+                .build());
+        });
+        return PagedResults.of(new ArrayListTotal<>(triggers, triggerContexts.getTotal()));
+    }
+
+    @Builder
+    @Getter
+    public static class Triggers {
+        AbstractTrigger abstractTrigger;
+        TriggerContext triggerContext;
     }
 
     @ExecuteOn(TaskExecutors.IO)


Test patch:
diff --git a/webserver/src/test/java/io/kestra/webserver/controllers/TriggerControllerTest.java b/webserver/src/test/java/io/kestra/webserver/controllers/TriggerControllerTest.java
index ff87066459..fad22f7c41 100644
--- a/webserver/src/test/java/io/kestra/webserver/controllers/TriggerControllerTest.java
+++ b/webserver/src/test/java/io/kestra/webserver/controllers/TriggerControllerTest.java
@@ -71,27 +71,30 @@ void search() {
         String triggerFlowId = "schedule-trigger-search";
         String triggerNamespace = "io.kestra.tests.schedule";
 
+        Flow flow = generateFlow(triggerFlowId);
+        jdbcFlowRepository.create(flow, flow.generateSource(), flow);
+
         Trigger trigger = Trigger.builder()
             .flowId(triggerFlowId)
             .namespace(triggerNamespace)
-            .triggerId("schedule-every-min")
+            .triggerId("trigger-nextexec-schedule")
             .date(ZonedDateTime.now())
             .build();
 
         jdbcTriggerRepository.save(trigger);
-        jdbcTriggerRepository.save(trigger.toBuilder().triggerId("schedule-5-min").build());
+        jdbcTriggerRepository.save(trigger.toBuilder().triggerId("trigger-nextexec-polling").build());
 
-        PagedResults<Trigger> triggers = client.toBlocking().retrieve(HttpRequest.GET("/api/v1/triggers/search?q=schedule-trigger-search&namespace=io.kestra.tests&sort=triggerId:asc"), Argument.of(PagedResults.class, Trigger.class));
+        PagedResults<TriggerController.Triggers> triggers = client.toBlocking().retrieve(HttpRequest.GET("/api/v1/triggers/search?q=schedule-trigger-search&namespace=io.kestra.tests&sort=triggerId:asc"), Argument.of(PagedResults.class, TriggerController.Triggers.class));
         assertThat(triggers.getTotal(), greaterThanOrEqualTo(2L));
 
-        assertThat(triggers.getResults(), Matchers.hasItems(
+        assertThat(triggers.getResults().stream().map(TriggerController.Triggers::getTriggerContext).toList(), Matchers.hasItems(
                 allOf(
-                    hasProperty("triggerId", is("schedule-every-min")),
+                    hasProperty("triggerId", is("trigger-nextexec-schedule")),
                     hasProperty("namespace", is(triggerNamespace)),
                     hasProperty("flowId", is(triggerFlowId))
                 ),
                 allOf(
-                    hasProperty("triggerId", is("schedule-5-min")),
+                    hasProperty("triggerId", is("trigger-nextexec-polling")),
                     hasProperty("namespace", is(triggerNamespace)),
                     hasProperty("flowId", is(triggerFlowId))
                 )
@@ -183,15 +186,15 @@ void nextExecutionDate() throws InterruptedException, TimeoutException {
             Duration.ofMillis(100),
             Duration.ofMinutes(2)
         );
-        PagedResults<Trigger> triggers = client.toBlocking().retrieve(HttpRequest.GET("/api/v1/triggers/search?q=trigger-nextexec"), Argument.of(PagedResults.class, Trigger.class));
-        assertThat(triggers.getResults().get(0).getNextExecutionDate(), notNullValue());
-        assertThat(triggers.getResults().get(1).getNextExecutionDate(), notNullValue());
+        PagedResults<TriggerController.Triggers> triggers = client.toBlocking().retrieve(HttpRequest.GET("/api/v1/triggers/search?q=trigger-nextexec"), Argument.of(PagedResults.class, TriggerController.Triggers.class));
+        assertThat(triggers.getResults().get(0).getTriggerContext().getNextExecutionDate(), notNullValue());
+        assertThat(triggers.getResults().get(1).getTriggerContext().getNextExecutionDate(), notNullValue());
     }
 
     private Flow generateFlow(String flowId) {
         return Flow.builder()
             .id(flowId)
-            .namespace("io.kestra.tests.scheduler")
+            .namespace("io.kestra.tests.schedule")
             .tasks(Collections.singletonList(Return.builder()
                 .id("task")
                 .type(Return.class.getName())
@@ -210,4 +213,6 @@ private Flow generateFlow(String flowId) {
             ))
             .build();
     }
+
+
 }