Repository: kestra-io/kestra
Issue: Highlight/Move Flow syntax error/validation icon
Labels: enhancement
frontend

### Feature description

The icon indicating flow syntax error/validation is a bit hided and could be closer to the actual editor or highlight messages in a better way.

One idea is to move this icon to the left side (close to the unfold/fold arrows) but then it won't appear in full topology view 

![image](https://github.com/kestra-io/kestra/assets/46634684/0449f660-b37d-4266-8acb-d6225ff8634f)


Issue comments:
Another suggestion would be to have a more explicit "with-text" button either fixed at top or bottom (without it taking too much space) because this functionnality is way too hidden imo. Also, it shouldn't appear as a button ?
Proper design/solution in Figma

https://www.figma.com/file/4tLhsoCsh9yVzYRY1N70kS/App-Kestra?type=design&node-id=147%3A784&t=zy4kqMTgQpMqckZe-1

Pull request review comments:
```suggestion
                <el-header>
                    <AlertCircle class="align-middle text-danger" />
                    <span class="align-middle">
                        {{ $t("error detected") }}
                    </span>
                </el-header>
```

Patch that resolved the issue:
diff --git a/ui/src/components/flows/ValidationError.vue b/ui/src/components/flows/ValidationError.vue
index 91efd35cfc..6073d6c1d2 100644
--- a/ui/src/components/flows/ValidationError.vue
+++ b/ui/src/components/flows/ValidationError.vue
@@ -1,21 +1,31 @@
 <template>
-    <el-tooltip :disabled="!error" raw-content transition="" :hide-after="0" :persistent="false">
+    <el-tooltip popper-class="p-0 bg-transparent" :placement="tooltipPlacement" :show-arrow="false" :disabled="!error" raw-content transition="" :persistent="false">
         <template #content>
-            <pre>{{ error }}</pre>
+            <el-container class="error-tooltip">
+                <el-header>
+                    <AlertCircle class="align-middle text-danger" />
+                    <span class="align-middle">
+                        {{ $t("error detected") }}
+                    </span>
+                </el-header>
+                <el-main>{{ error }}</el-main>
+            </el-container>
         </template>
-        <el-button :link="link" type="default">
-            <component :class="'text-' + (error ? 'danger' : 'success')" :is="error ? CloseCircleOutline : CheckCircleOutline" />
+        <el-button v-bind="$attrs" :link="link" :size="size" type="default">
+            <component :class="'text-' + (error ? 'danger' : 'success')" :is="error ? AlertCircle : CheckCircle" />
+            <span v-if="error" class="text-danger">{{ $t("error detected") }}</span>
         </el-button>
     </el-tooltip>
 </template>
 
 <script setup>
-    import CheckCircleOutline from "vue-material-design-icons/CheckCircleOutline.vue";
-    import CloseCircleOutline from "vue-material-design-icons/CloseCircleOutline.vue";
+    import CheckCircle from "vue-material-design-icons/CheckCircle.vue";
+    import AlertCircle from "vue-material-design-icons/AlertCircle.vue";
 </script>
 
 <script>
     export default {
+        inheritAttrs: false,
         props: {
             error: {
                 type: String,
@@ -25,26 +35,94 @@
                 type: Boolean,
                 default: false
             },
+            size: {
+                type: String,
+                default: "default"
+            },
+            tooltipPlacement: {
+                type: String,
+                default: undefined
+            }
         },
 
     };
 </script>
 
 
-<style lang="scss" scoped>
-    pre {
-        margin-bottom: 0;
-        max-width: 40vw;
-        white-space: pre-wrap;
-        background: transparent;
-        color: var(--bs-gray-100);
-        html.dark & {
-            color: var(--bs-gray-900);
+<style scoped lang="scss">
+    @import "../../styles/variable";
+
+    .el-button.el-button--default {
+        transition: none;
+
+        &.el-button--small {
+            padding: 5px;
+            height: fit-content;
+        }
+
+        &:hover, &:focus {
+            background-color: var(--el-button-bg-color);
+        }
+
+        &:has(.material-design-icon.text-success) {
+            border-color: rgb(var(--bs-success-rgb));
+        }
+        &:has(.material-design-icon.text-danger) {
+            border-color: rgb(var(--bs-danger-rgb));
+
+            span.text-danger:not(.material-design-icon) {
+                margin-left: calc(var(--spacer) / 2);
+                font-size: $font-size-sm;
+            }
         }
-        padding: 0;
     }
 
-    .el-button.el-button--default {
-        background-color: var(--bs-body-bg);
+    .error-tooltip {
+        padding: 0;
+        width: fit-content;
+        min-width: 20vw;
+        max-width: 50vw;
+        border-radius: $border-radius-lg;
+        color: $black;
+
+        html.dark & {
+            color: white;
+        }
+
+        > * {
+            height: fit-content;
+            margin: 0;
+        }
+
+        .el-header {
+            padding: $spacer;
+            background-color: var(--bs-gray-200);
+            border-bottom: 1px solid var(--bs-gray-300);
+            border-radius: $border-radius-lg $border-radius-lg 0 0;
+            font-size: $font-size-sm;
+            font-weight: $font-weight-bold;
+
+            html.dark & {
+                background-color: var(--bs-gray-500);
+                border-bottom: 1px solid var(--bs-gray-600);
+            }
+
+            .material-design-icon {
+                font-size: 1.5rem;
+                margin-right: calc(var(--spacer) / 2);
+            }
+        }
+
+        .el-main {
+            padding: calc(2 * var(--spacer)) $spacer !important;
+            border-radius: 0 0 $border-radius-lg $border-radius-lg;
+            font-family: $font-family-monospace;
+            background-color: white;
+
+            html.dark & {
+                color: white;
+                background-color: var(--bs-gray-400);
+            }
+        }
     }
 </style>
\ No newline at end of file
diff --git a/ui/src/components/inputs/Editor.vue b/ui/src/components/inputs/Editor.vue
index f5047edd5c..d16d6cc50d 100644
--- a/ui/src/components/inputs/Editor.vue
+++ b/ui/src/components/inputs/Editor.vue
@@ -20,6 +20,7 @@
                             <el-button :icon="icon.Help" @click="restartGuidedTour" size="small" />
                         </el-tooltip>
                     </el-button-group>
+                    <slot name="extends-navbar"/>
                 </div>
             </slot>
         </nav>
diff --git a/ui/src/components/inputs/EditorView.vue b/ui/src/components/inputs/EditorView.vue
index 154d512425..d113955b43 100644
--- a/ui/src/components/inputs/EditorView.vue
+++ b/ui/src/components/inputs/EditorView.vue
@@ -619,7 +619,11 @@
             @cursor="updatePluginDocumentation($event)"
             :creating="isCreating"
             @restartGuidedTour="() => persistViewType('source')"
-        />
+        >
+            <template #extends-navbar>
+                <ValidationError tooltip-placement="bottom-start" size="small" class="ms-2" :error="flowError" />
+            </template>
+        </editor>
         <div class="slider" @mousedown="dragEditor" v-if="combinedEditor" />
         <Blueprints :class="{'d-none': viewType !== 'source-blueprints'}" embed class="combined-right-view enhance-readability" :top-navbar="false" prevent-route-info />
         <div
@@ -639,7 +643,11 @@
                 :source="flowYaml"
                 :is-allowed-edit="isAllowedEdit()"
                 :view-type="viewType"
-            />
+            >
+                <template #top-bar v-if="viewType === 'topology'">
+                    <ValidationError tooltip-placement="bottom-start" size="small" class="ms-2" :error="flowError" />
+                </template>
+            </LowCodeEditor>
         </div>
         <PluginDocumentation
             v-if="viewType === 'source-doc'"
diff --git a/ui/src/components/inputs/LowCodeEditor.vue b/ui/src/components/inputs/LowCodeEditor.vue
index c54b03ebd5..8d0a33cb95 100644
--- a/ui/src/components/inputs/LowCodeEditor.vue
+++ b/ui/src/components/inputs/LowCodeEditor.vue
@@ -540,6 +540,7 @@
 
 <template>
     <div ref="vueFlow" class="vueflow">
+        <slot name="top-bar"/>
         <VueFlow
             v-model="elements"
             :default-marker-color="cssVariable('--bs-cyan')"
diff --git a/ui/src/components/inputs/SwitchView.vue b/ui/src/components/inputs/SwitchView.vue
index 67635946b4..6ec5b5c18a 100644
--- a/ui/src/components/inputs/SwitchView.vue
+++ b/ui/src/components/inputs/SwitchView.vue
@@ -1,6 +1,5 @@
 <template>
     <el-button-group size="small">
-        <ValidationError :error="flowError" />
         <el-tooltip :content="$t('source')" transition="" :hide-after="0" :persistent="false">
             <el-button :type="buttonType('source')" @click="switchView('source')" :icon="FileDocumentEdit" />
         </el-tooltip>
@@ -28,7 +27,6 @@
 </script>
 
 <script>
-    import {mapGetters} from "vuex";
     import ValidationError from "../flows/ValidationError.vue";
 
     export default {
@@ -39,9 +37,6 @@
                 type: String
             }
         },
-        computed: {
-            ...mapGetters("flow", ["flowError"]),
-        },
         methods: {
             switchView(view) {
                 this.$emit("switch-view", view)
diff --git a/ui/src/styles/layout/element-plus-overload.scss b/ui/src/styles/layout/element-plus-overload.scss
index 9f2e570e2d..ccffe73f9f 100644
--- a/ui/src/styles/layout/element-plus-overload.scss
+++ b/ui/src/styles/layout/element-plus-overload.scss
@@ -393,6 +393,8 @@ form.ks-horizontal {
 
 // popper
 .el-popper {
+    border-radius: $border-radius-lg;
+
     &.is-light {
         border: 1px solid var(--bs-border-color);
 
diff --git a/ui/src/translations.json b/ui/src/translations.json
index dcf29e2661..5e08e2fe1a 100644
--- a/ui/src/translations.json
+++ b/ui/src/translations.json
@@ -454,7 +454,8 @@
     "expand all": "Expand all",
     "collapse all": "Collapse all",
     "log expand setting": "Log default display",
-    "slack support": "Ask help on our Slack"
+    "slack support": "Ask help on our Slack",
+    "error detected": "Error detected"
   },
   "fr": {
     "id": "Identifiant",
@@ -913,6 +914,7 @@
     "expand all": "Afficher tout",
     "collapse all": "Masquer tout",
     "log expand setting": "Affichage par défaut des journaux",
-    "slack support": "Demandez de l'aide sur notre Slack"
+    "slack support": "Demandez de l'aide sur notre Slack",
+    "error detected": "Erreur détectée"
   }
 }


Test patch: