Repository: kestra-io/kestra
Issue: Add a setting for log collapsing 
Labels: bug

### Expected Behavior

![image](https://github.com/kestra-io/kestra/assets/86264395/142ea710-6a5e-4956-99b0-e7f74f1530e8)

I need to manually click on each task to expand and show the logs -- this seems like a regression bug

![image](https://github.com/kestra-io/kestra/assets/86264395/ec209dc1-59a1-46a3-8b5e-2a648b3c4285)


### Actual Behaviour

_No response_

### Steps To Reproduce

_No response_

### Environment Information

- Kestra Version: 0.10.0 

### Example flow

_No response_

Issue comments:
it was an intended feature and flow will expand only on failed tasks now (like GHA), WDYT ?
I want to see my logs, this is the most important aspect. 

Having to expand/collapse is quite inconvenient. Imagine you want to quickly scan the logs from the entire workflow that has 20 tasks. You would need to click on expand 20 times...

Would it be possible to provide that as a UI Setting that can be configured? I can imagine some people might prefer this behavior that exists now, but I think the vast majority would believe that something must be wrong with the workflow because the logs are not showing up 

Logs streaming in by default are especially useful to see the status of a workflow being in progress
I think we should keep the collapsed by default (new intended feature) as it definitely keep things tidy. Logs are mostly a debugging feature (in any tools) - when you flow works you don't look at the logs, it's mostly for debugging error (and so you want to quickly find the ERROR log, hence the new behavior)

But agree with a settings where you can we set "Expand all logs" = true | false (false by default)
but I don't see the progress... this makes iterating on building workflows way more difficult. Troubleshooting is not only if something failed, but also the flow may succeed but the log shows something unexpected, or not showing some logs

this seems like a big regression in developer productivity

to see a tidy view of task structure, I would go to the topology or Gantt view. Logs tab is to see the logs.


to find a middle ground, perhaps we may consider adding a button allowing users to expand all @Ben8t?

![image](https://github.com/kestra-io/kestra/assets/86264395/4d4a1564-69fe-489f-9e45-18a54cf702ce)

Yes I think it's good compromise !
Changing the issue title to be clearer
I too prefer logs to be openned by default, easier when building a workflow.
My opinion is that the logs tab should display ... logs, today it displays tasks status with call to action, one of these call to action is to show the task log.
Add a settings with : 
- Expands logs (default one)
- Collapse logs
- Expands only error 

Add a collapse all / expand all on the logs page (that will not affect the settings) 

Pull request review comments:
indent

Patch that resolved the issue:
diff --git a/ui/src/components/executions/Logs.vue b/ui/src/components/executions/Logs.vue
index 31ab01384e..2121520737 100644
--- a/ui/src/components/executions/Logs.vue
+++ b/ui/src/components/executions/Logs.vue
@@ -18,6 +18,11 @@
                     @update:model-value="onChange"
                 />
             </el-form-item>
+            <el-form-item>
+                <el-button @click="handleLogDisplay()">
+                    {{ logDisplayButtonText }}
+                </el-button>
+            </el-form-item>
             <el-form-item>
                 <el-button-group>
                     <el-button @click="downloadContent()">
@@ -35,6 +40,7 @@
             :exclude-metas="['namespace', 'flowId', 'taskId', 'executionId']"
             :filter="filter"
             @follow="forwardEvent('follow', $event)"
+            :logs-to-open-parent="logsToOpen"
         />
         <div v-else-if="execution.state.current !== State.RUNNING">
             <log-list
@@ -46,6 +52,7 @@
                 :exclude-metas="['namespace', 'flowId', 'taskId', 'executionId']"
                 :filter="filter"
                 @follow="forwardEvent('follow', $event)"
+                :logs-to-open-parent="logsToOpen"
             />
         </div>
     </div>
@@ -60,6 +67,7 @@
     import LogLevelSelector from "../logs/LogLevelSelector.vue";
     import Collapse from "../layout/Collapse.vue";
     import State from "../../utils/state";
+    import {logDisplayTypes} from "../../utils/constants";
 
     export default {
         components: {
@@ -74,7 +82,8 @@
             return {
                 fullscreen: false,
                 level: undefined,
-                filter: undefined
+                filter: undefined,
+                logsToOpen: undefined
             };
         },
         created() {
@@ -100,7 +109,15 @@
                     }
                 }
                 return fullList
-            }
+            },
+            logDisplayButtonText() {
+                if (this.logsToOpen) {
+                    return this.logsToOpen.length !== 0 ? this.$t("collapse all") : this.$t("expand all")
+                } else {
+                    const logDisplay =  localStorage.getItem("logDisplay") || logDisplayTypes.DEFAULT;
+                    return logDisplay === logDisplayTypes.HIDDEN ? this.$t("expand all") : this.$t("collapse all")
+                }
+            },
         },
         methods: {
             downloadContent() {
@@ -127,6 +144,18 @@
             onChange() {
                 this.$router.push({query: {...this.$route.query, q: this.filter, level: this.level, page: 1}});
             },
+            handleLogDisplay() {
+                const logDisplay =  localStorage.getItem("logDisplay") || logDisplayTypes.DEFAULT;
+                if (this.logsToOpen) {
+                    this.logsToOpen.length === 0 ? this.logsToOpen = State.arrayAllStates().map(s => s.name) : this.logsToOpen = []
+                    return;
+                }
+                if (logDisplay === logDisplayTypes.HIDDEN ) {
+                    this.logsToOpen = State.arrayAllStates().map(s => s.name)
+                } else {
+                    this.logsToOpen = []
+                }
+            }
         }
     };
 </script>
diff --git a/ui/src/components/logs/LogList.vue b/ui/src/components/logs/LogList.vue
index 3f985a3d50..4c42263237 100644
--- a/ui/src/components/logs/LogList.vue
+++ b/ui/src/components/logs/LogList.vue
@@ -177,6 +177,7 @@
     import FlowUtils from "../../utils/flowUtils.js";
     import moment from "moment";
     import "vue-virtual-scroller/dist/vue-virtual-scroller.css"
+    import {logDisplayTypes} from "../../utils/constants";
 
 
     export default {
@@ -228,6 +229,10 @@
             attempt: {
                 type: Number,
                 default: undefined
+            },
+            logsToOpenParent: {
+                type: Array,
+                default: undefined
             }
         },
         data() {
@@ -245,8 +250,7 @@
                 followLogs: [],
                 logsTotal: 0,
                 timer: undefined,
-                timeout: undefined,
-                logsToOpen: [State.FAILED, State.CREATED, State.RUNNING]
+                timeout: undefined
             };
         },
         watch: {
@@ -261,7 +265,10 @@
                 }
             },
             currentTaskRuns: function () {
-                this.openTaskRun();
+                this.openTaskRun(this.logsToOpen);
+            },
+            logsToOpenParent: function () {
+                this.openTaskRun(this.logsToOpenParent);
             }
         },
         mounted() {
@@ -269,7 +276,7 @@
                 this.loadLogs();
             }
             if (this.logsToOpen.includes(this.execution.state.current)) {
-                this.openTaskRun();
+                this.openTaskRun(this.logsToOpen);
             }
         },
         computed: {
@@ -313,12 +320,31 @@
                     .map((e, index) => {
                         return {...e, index: index}
                     })
-            }
+            },
+            logsToOpen() {
+                if (this.logsToOpenParent) {
+                    return this.logsToOpenParent
+                }
+                switch(localStorage.getItem("logDisplay") || logDisplayTypes.DEFAULT){
+                    case logDisplayTypes.ERROR:
+                        return [State.FAILED, State.RUNNING, State.PAUSED]
+                    case logDisplayTypes.ALL:
+                        return State.arrayAllStates().map(s => s.name)
+                    case logDisplayTypes.HIDDEN:
+                        return []
+                    default:
+                        return State.arrayAllStates().map(s => s.name)
+                }
+            },
         },
         methods: {
-            openTaskRun(){
+            openTaskRun(logsToOpen){
                 this.currentTaskRuns.forEach((taskRun) => {
-                    if (this.logsToOpen.includes(taskRun.state.current)) {
+                    if (logsToOpen.length === 0) {
+                        this.showLogs = []
+                        return;
+                    }
+                    if (logsToOpen.includes(taskRun.state.current)) {
                         const attemptNumber = taskRun.attempts ? taskRun.attempts.length - 1 : 0
                         this.showLogs.push(`${taskRun.id}-${attemptNumber}`)
                         this?.$refs?.[`${taskRun.id}-${attemptNumber}`]?.[0]?.scrollToBottom();
diff --git a/ui/src/components/settings/Settings.vue b/ui/src/components/settings/Settings.vue
index bf80a6d923..412afa8ebf 100644
--- a/ui/src/components/settings/Settings.vue
+++ b/ui/src/components/settings/Settings.vue
@@ -55,8 +55,15 @@
                 </el-button>
             </el-form-item>
 
-            <el-form-item :label="$t('show documentation')">
-                <el-checkbox :label="$t('show task documentation in editor')" :model-value="editorDocumentation" @update:model-value="onEditorDocumentation" />
+            <el-form-item :label="$t('log expand setting')">
+                <el-select :model-value="logDisplay" @update:model-value="onLogDisplayChange">
+                    <el-option
+                        v-for="item in logDisplayOptions"
+                        :key="item.value"
+                        :label="item.text"
+                        :value="item.value"
+                    />
+                </el-select>
             </el-form-item>
         </el-form>
     </div>
@@ -74,6 +81,7 @@
     import {mapGetters, mapState} from "vuex";
     import permission from "../../models/permission";
     import action from "../../models/action";
+    import {logDisplayTypes} from "../../utils/constants";
 
     export default {
         mixins: [RouteContext],
@@ -90,7 +98,7 @@
                 editorTheme: undefined,
                 autofoldTextEditor: undefined,
                 guidedTour: undefined,
-                editorDocumentation: undefined
+                logDisplay: undefined
             };
         },
         created() {
@@ -103,7 +111,7 @@
             this.editorTheme = localStorage.getItem("editorTheme") || (darkTheme ? "dark" : "vs");
             this.autofoldTextEditor = localStorage.getItem("autofoldTextEditor") === "true";
             this.guidedTour = localStorage.getItem("tourDoneOrSkip") === "true";
-            this.editorDocumentation = localStorage.getItem("editorDocumentation") !== "false";
+            this.logDisplay = localStorage.getItem("logDisplay") || logDisplayTypes.DEFAULT;
         },
         methods: {
             onNamespaceSelect(value) {
@@ -148,12 +156,6 @@
                 this.autofoldTextEditor = value;
                 this.$toast().saved();
             },
-            onEditorDocumentation(value){
-                localStorage.setItem("editorDocumentation", value);
-                this.editorDocumentation = value;
-                this.$toast().saved();
-
-            },
             exportFlows() {
                 return this.$store
                     .dispatch("flow/exportFlowByQuery", {})
@@ -168,6 +170,11 @@
                         this.$toast().success(this.$t("templates exported"));
                     })
             },
+            onLogDisplayChange(value) {
+                localStorage.setItem("logDisplay", value);
+                this.logDisplay = value;
+                this.$toast().saved();
+            }
         },
         computed: {
             ...mapGetters("core", ["guidedProperties"]),
@@ -201,6 +208,13 @@
             canReadTemplates() {
                 return this.user && this.user.isAllowed(permission.TEMPLATE, action.READ);
             },
+            logDisplayOptions() {
+                return  [
+                    {value: logDisplayTypes.ERROR, text: this.$t("expand error")},
+                    {value: logDisplayTypes.ALL, text: this.$t("expand all")},
+                    {value: logDisplayTypes.HIDDEN, text: this.$t("collapse all")}
+                ]
+            },
         }
     };
 </script>
diff --git a/ui/src/translations.json b/ui/src/translations.json
index f32c75b834..20cb3087f7 100644
--- a/ui/src/translations.json
+++ b/ui/src/translations.json
@@ -449,7 +449,11 @@
         "existing": "A <code>{flowFullName}</code> Flow draft was retrieved, do you want to resume its edition ?"
       }
     },
-    "title": "Title"
+    "title": "Title",
+    "expand error": "Expand only failed task",
+    "expand all": "Expand all",
+    "collapse all": "Collapse all",
+    "log expand setting": "Log default display"
   },
   "fr": {
     "id": "Identifiant",
@@ -903,6 +907,10 @@
         "existing": "Un brouillon pour le Flow <code>{flowFullName}</code> existe, voulez-vous reprendre son édition?"
       }
     },
-    "title": "Titre"
+    "title": "Titre",
+    "expand error": "Afficher uniquement les erreurs",
+    "expand all": "Afficher tout",
+    "collapse all": "Masquer tout",
+    "log expand setting": "Affichage par défaut des journaux"
   }
 }
diff --git a/ui/src/utils/constants.js b/ui/src/utils/constants.js
index b16ffc85e6..af4cbe71c1 100644
--- a/ui/src/utils/constants.js
+++ b/ui/src/utils/constants.js
@@ -6,4 +6,11 @@ export const SECTIONS = {
 export const stateGlobalChartTypes = {
     EXECUTIONS: "executions",
     TASKRUNS: "taskruns"
+}
+
+export const logDisplayTypes = {
+    ALL: "all",
+    ERROR: "error",
+    HIDDEN: "hidden",
+    DEFAULT: "all"
 }
\ No newline at end of file
diff --git a/ui/src/utils/state.js b/ui/src/utils/state.js
index eab93b3fea..6fb690b8c5 100644
--- a/ui/src/utils/state.js
+++ b/ui/src/utils/state.js
@@ -147,6 +147,10 @@ export default class State {
         });
     }
 
+    static arrayAllStates() {
+        return Object.values(STATE);
+    }
+
     static colorClass() {
         return _mapValues(STATE, state => state.colorClass);
     }


Test patch:
diff --git a/core/src/test/java/io/kestra/core/schedulers/SchedulerScheduleTest.java b/core/src/test/java/io/kestra/core/schedulers/SchedulerScheduleTest.java
index 86a3639931..989a99198a 100644
--- a/core/src/test/java/io/kestra/core/schedulers/SchedulerScheduleTest.java
+++ b/core/src/test/java/io/kestra/core/schedulers/SchedulerScheduleTest.java
@@ -7,7 +7,7 @@
 import io.kestra.core.runners.FlowListeners;
 import io.kestra.core.runners.Worker;
 import jakarta.inject.Inject;
-import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.RetryingTest;
 
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
@@ -62,7 +62,7 @@ protected AbstractScheduler scheduler(FlowListeners flowListenersServiceSpy, Sch
         );
     }
 
-    @Test
+    @RetryingTest(5)
     void schedule() throws Exception {
         // mock flow listeners
         FlowListeners flowListenersServiceSpy = spy(this.flowListenersService);