Repository: kestra-io/kestra
Issue: Admin tabs as submenu items
Labels: enhancement

### Feature description

Trigger administration should be a submenu instead of a tab (like documentation menu)

Issue comments:
Maybe we could also put Users, Groups and Roles (and namespaces?) in there as well ?
Hello ! The PR contains that already 👍 

Pull request review comments:

Patch that resolved the issue:
diff --git a/jdbc/src/main/java/io/kestra/jdbc/repository/AbstractJdbcTriggerRepository.java b/jdbc/src/main/java/io/kestra/jdbc/repository/AbstractJdbcTriggerRepository.java
index d6df1be232..b6976c532b 100644
--- a/jdbc/src/main/java/io/kestra/jdbc/repository/AbstractJdbcTriggerRepository.java
+++ b/jdbc/src/main/java/io/kestra/jdbc/repository/AbstractJdbcTriggerRepository.java
@@ -102,12 +102,15 @@ public ArrayListTotal<Trigger> find(Pageable pageable, String query, String name
                     .select(field("value"))
                     .hint(context.dialect() == SQLDialect.MYSQL ? "SQL_CALC_FOUND_ROWS" : null)
                     .from(this.jdbcRepository.getTable())
-                    .where(this.fullTextCondition(query));
+                    .where(this.fullTextCondition(query))
+                    .and(this.defaultFilter());
 
                 if (namespace != null) {
                     select.and(DSL.or(field("namespace").eq(namespace), field("namespace").likeIgnoreCase(namespace + ".%")));
                 }
 
+                select.and(this.defaultFilter());
+
                 return this.jdbcRepository.fetchPage(context, select, pageable);
             });
     }
@@ -116,6 +119,11 @@ protected Condition fullTextCondition(String query) {
         return query == null ? DSL.trueCondition() : jdbcRepository.fullTextCondition(List.of("fulltext"), query);
     }
 
+    @Override
+    protected Condition defaultFilter() {
+        return DSL.trueCondition();
+    }
+
     @Override
     public Function<String, String> sortMapping() throws IllegalArgumentException {
         Map<String, String> mapper = Map.of(
diff --git a/ui/src/components/admin/Triggers.vue b/ui/src/components/admin/Triggers.vue
index 46d0b6f918..4aeb1681a5 100644
--- a/ui/src/components/admin/Triggers.vue
+++ b/ui/src/components/admin/Triggers.vue
@@ -86,7 +86,7 @@
                                 {{ scope.row.evaluateRunningDate ? $filters.date(scope.row.evaluateRunningDate, "iso") : "" }}
                             </template>
                         </el-table-column>
-                        <el-table-column column-key="action" class-name="row-action">
+                        <el-table-column v-if="user.hasAnyAction(permission.FLOW, action.UPDATE)" column-key="action" class-name="row-action">
                             <template #default="scope">
                                 <el-button text v-if="scope.row.executionId || scope.row.evaluateRunningDate">
                                     <kicon
@@ -120,6 +120,8 @@
 <script setup>
     import LockOff from "vue-material-design-icons/LockOff.vue";
     import Kicon from "../Kicon.vue";
+    import permission from "../../models/permission";
+    import action from "../../models/action";
 </script>
 <script>
     import NamespaceSelect from "../namespace/NamespaceSelect.vue";
@@ -130,6 +132,7 @@
     import DataTableActions from "../../mixins/dataTableActions";
     import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
     import RefreshButton from "../layout/RefreshButton.vue";
+    import {mapState} from "vuex";
 
     export default {
         mixins: [RouteContext, RestoreUrl, DataTableActions],
@@ -172,7 +175,7 @@
                 });
 
                 this.$message({
-                    message: this.$t("trigger unlocked"),
+                    message: this.$t("unlock trigger.success"),
                     type: "success"
                 });
 
@@ -183,6 +186,14 @@
 
                 this.triggerToUnlock = undefined;
             }
+        },
+        computed: {
+            ...mapState("auth", ["user"]),
+            routeInfo() {
+                return {
+                    title: this.$t("triggers")
+                }
+            }
         }
     };
 </script>
\ No newline at end of file
diff --git a/ui/src/models/permission.js b/ui/src/models/permission.js
index 24c4167aa7..e5aee58e80 100644
--- a/ui/src/models/permission.js
+++ b/ui/src/models/permission.js
@@ -1,6 +1,5 @@
 export default {
     FLOW: "FLOW",
     EXECUTION: "EXECUTION",
-    TEMPLATE: "TEMPLATE",
-    ADMIN: "ADMIN"
+    TEMPLATE: "TEMPLATE"
 }
diff --git a/ui/src/override/components/LeftMenu.vue b/ui/src/override/components/LeftMenu.vue
index f1711ed831..2ebc880455 100644
--- a/ui/src/override/components/LeftMenu.vue
+++ b/ui/src/override/components/LeftMenu.vue
@@ -3,8 +3,6 @@
         id="side-menu"
         :menu="menu"
         @update:collapsed="onToggleCollapse"
-        :show-one-child="true"
-        :show-child="showChildren"
         width="268px"
         :collapsed="collapsed"
     >
@@ -48,6 +46,7 @@
     import CogOutline from "vue-material-design-icons/CogOutline.vue";
     import ViewDashboardVariantOutline from "vue-material-design-icons/ViewDashboardVariantOutline.vue";
     import FileDocumentArrowRightOutline from "vue-material-design-icons/FileDocumentArrowRightOutline.vue";
+    import TimerCogOutline from "vue-material-design-icons/TimerCogOutline.vue";
     import {mapState} from "vuex";
 
     export default {
@@ -68,22 +67,17 @@
                 return items
                     .map(r => {
                         if (r.href === this.$route.path) {
-                            r.disabled = true
+                            r.disabled = true;
                         }
 
-                        if (r.href !== "/" && this.$route.path.startsWith(r.href)) {
-                            r.class = "vsm--link_active"
+                        // route hack is still needed for blueprints
+                        if (r.href !== "/" && (this.$route.path.startsWith(r.href) || r.routes?.includes(this.$route.name))) {
+                            r.class = "vsm--link_active";
                         }
 
-                        // special case for plugins, were parents have no href to set active
-                        // Could be adapted to all routes to have something more generic
-                        if (r.routes?.includes(this.$route.name)) {
-                            r.class = "vsm--link_active"
-
-                            if (r.child) {
-                                this.showChildren = true
-                                r.child = this.disabledCurrentRoute(r.child)
-                            }
+                        if (r.child && r.child.some(c => this.$route.path.startsWith(c.href))) {
+                            r.class = "vsm--link_active";
+                            r.child = this.disabledCurrentRoute(r.child);
                         }
 
                         return r;
@@ -155,7 +149,6 @@
                             element: BookMultipleOutline,
                             class: "menu-icon"
                         },
-                        routes: ["plugins/view"],
                         child: [
                             {
                                 href: "https://kestra.io/docs/",
@@ -169,7 +162,6 @@
                             {
                                 href: "/plugins",
                                 title: this.$t("plugins.names"),
-                                routes: ["plugins/view"],
                                 icon: {
                                     element: GoogleCirclesExtended,
                                     class: "menu-icon"
@@ -214,16 +206,27 @@
                         }
                     },
                     {
-                        href: "/admin",
                         title: this.$t("administration"),
                         icon: {
                             element: AccountSupervisorOutline,
                             class: "menu-icon"
-                        }
+                        },
+                        child: [
+                            {
+                                href: "/admin/triggers",
+                                title: this.$t("triggers"),
+                                icon: {
+                                    element: TimerCogOutline,
+                                    class: "menu-icon"
+                                }
+                            }
+                        ]
                     }
                 ];
+            },
+            expandParentIfNeeded() {
+                document.querySelectorAll(".vsm--link_level-1.vsm--link_active:not(.vsm--link_open)[aria-haspopup]").forEach(e => e.click());
             }
-
         },
         watch: {
             menu: {
@@ -232,7 +235,7 @@
                         //empty icon name on mouseover
                         e.setAttribute("title", "")
                     });
-                    this.showChildren = false
+                    this.expandParentIfNeeded();
                 },
                 flush: 'post'
             }
@@ -240,7 +243,6 @@
         data() {
             return {
                 collapsed: localStorage.getItem("menuCollapsed") === "true",
-                showChildren: false
             };
         },
         computed: {
@@ -248,6 +250,9 @@
             menu() {
                 return this.disabledCurrentRoute(this.generateMenu());
             }
+        },
+        mounted() {
+            this.expandParentIfNeeded();
         }
     };
 </script>
diff --git a/ui/src/override/components/admin/Admin.vue b/ui/src/override/components/admin/Admin.vue
deleted file mode 100644
index 4c47fa8674..0000000000
--- a/ui/src/override/components/admin/Admin.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-    <div>
-        <tabs route-name="admin" ref="currentTab" :tabs="tabs" />
-    </div>
-</template>
-
-<script>
-    import RouteContext from "../../../mixins/routeContext";
-    import Tabs from "../../../components/Tabs.vue";
-    import Triggers from "../../../components/admin/Triggers.vue";
-
-    export default {
-        mixins: [RouteContext],
-        components: {
-            Tabs
-        },
-        data() {
-            return {
-                tabs: this.getTabs()
-            }
-        },
-        methods: {
-            activeTabName() {
-                if (this.$refs.currentTab) {
-                    return this.$refs.currentTab.activeTab.name || "triggers";
-                }
-
-                return null;
-            },
-            getTabs() {
-                return [
-                    {
-                        name: "triggers",
-                        component: Triggers,
-                        title: this.$t("triggers"),
-                    }
-                ]
-            }
-        },
-        computed: {
-            routeInfo() {
-                return {
-                    title: this.$t("administration")
-                }
-            }
-        }
-    };
-</script>
\ No newline at end of file
diff --git a/ui/src/routes/routes.js b/ui/src/routes/routes.js
index a2893a4e1a..3f4c57e7bb 100644
--- a/ui/src/routes/routes.js
+++ b/ui/src/routes/routes.js
@@ -16,7 +16,7 @@ import FlowCreate from "../components/flows/FlowCreate.vue";
 import FlowMetrics from "../components/flows/FlowMetrics.vue";
 import Blueprints from "override/components/flows/blueprints/Blueprints.vue";
 import BlueprintDetail from "../components/flows/blueprints/BlueprintDetail.vue";
-import Admin from "override/components/admin/Admin.vue";
+import Triggers from "../components/admin/Triggers.vue";
 
 export default [
     //Flows
@@ -55,7 +55,7 @@ export default [
     {name: "settings", path: "/settings", component: Settings},
 
     //Admin
-    {name: "admin", path: "/admin/:tab?", component: Admin},
+    {name: "admin/triggers", path: "/admin/triggers", component: Triggers},
 
     //Errors
     {name: "errors/404-wildcard", path: "/:pathMatch(.*)", component: Errors, props: {code: 404}},
diff --git a/ui/src/translations.json b/ui/src/translations.json
index e5dc5b2b99..7370954455 100644
--- a/ui/src/translations.json
+++ b/ui/src/translations.json
@@ -476,7 +476,8 @@
       },
       "confirmation": "Are you sure you want to unlock the trigger ?",
       "warning": "It could lead to concurrent executions for the same trigger and should be considered as a last resort option.",
-      "button": "Unlock trigger"
+      "button": "Unlock trigger",
+      "success": "Trigger is unlocked"
     },
     "date format": "Date format",
     "timezone": "Timezone"
@@ -958,7 +959,8 @@
       },
       "confirmation": "Êtes vous sûr(e) de vouloir débloquer le déclencheur ?",
       "warning": "Cela pourrait amener à de multiples exécutions concurrentes pour le même déclencheur et devrait être considéré comme une solution de dernier recours.",
-      "button": "Débloquer le déclencheur"
+      "button": "Débloquer le déclencheur",
+      "success": "Le déclencheur est débloqué"
     },
     "date format": "Format de date",
     "timezone": "Fuseau horaire"


Test patch: