This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git


The following commit(s) were added to refs/heads/main by this push:
     new 88cc989  ui: grant viewer/maintainer overview:read + menu-visibility 
matrix on Roles page
88cc989 is described below

commit 88cc989b1fb885424965b3c9e3653bd483a01750
Author: Wu Sheng <[email protected]>
AuthorDate: Wed May 20 17:07:59 2026 +0800

    ui: grant viewer/maintainer overview:read + menu-visibility matrix on Roles 
page
    
    - horizon.example.yaml: viewer and maintainer roles now include
      overview:read — the overview dashboards are read-only observability and
      belong in the base viewer experience (matches operator/admin which
      already had it). The Overviews sidebar section + /api/overview/* both
      gate on this verb, so they now work for viewers.
    - RolesView: add a "Menu visibility" matrix — sidebar item × role, ✔/—,
      computed live from the policy with the same verb gates AppSidebar uses
      (last column shows the gating read verb). Makes "what does this verb
      unlock in the nav" concrete per role; `null` verb = any signed-in user
      (the cap-gated data layers).
---
 apps/ui/src/features/admin/roles/RolesView.vue | 89 ++++++++++++++++++++++++++
 horizon.example.yaml                           |  8 ++-
 2 files changed, 94 insertions(+), 3 deletions(-)

diff --git a/apps/ui/src/features/admin/roles/RolesView.vue 
b/apps/ui/src/features/admin/roles/RolesView.vue
index 2a34cd2..2c79822 100644
--- a/apps/ui/src/features/admin/roles/RolesView.vue
+++ b/apps/ui/src/features/admin/roles/RolesView.vue
@@ -58,6 +58,36 @@ function hasVerb(grants: readonly string[], required: 
string): boolean {
 
 const roleNames = computed(() => Object.keys(status.value?.rbac.roles ?? 
{}).sort(rolePriority));
 
+/** Grants for a role from the live policy. */
+function grantsFor(role: string): readonly string[] {
+  return status.value?.rbac.roles?.[role] ?? [];
+}
+/** Sidebar menu → the read verb that gates its visibility (mirrors
+ *  AppSidebar.vue). Drives the visibility matrix so operators can read,
+ *  per role, exactly which navigation items appear. `null` verb = shown
+ *  to any signed-in user (the core data layers, cap-gated only). */
+const MENU_GATES: ReadonlyArray<{ label: string; verb: string | null }> = [
+  { label: 'Layers (per-layer data)', verb: null },
+  { label: 'Alarms', verb: 'alarms:read' },
+  { label: 'Overviews', verb: 'overview:read' },
+  { label: 'Cluster status', verb: 'cluster:read' },
+  { label: 'Metrics Inspect', verb: 'inspect:read' },
+  { label: 'Alerting rules', verb: 'alarm-rule:read' },
+  { label: 'Live debugger · Capture history', verb: 'live-debug:read' },
+  { label: 'DSL Management', verb: 'rule:read' },
+  { label: 'Overview templates', verb: 'overview:read' },
+  { label: 'Layer dashboards', verb: 'dashboard:read' },
+  { label: 'Alert page', verb: 'alarm-setup:read' },
+  { label: 'Global defaults', verb: 'setup:read' },
+  { label: 'Users', verb: 'user:read' },
+  { label: 'Auth status', verb: 'auth:read' },
+  { label: 'Roles & permissions', verb: 'role:read' },
+];
+/** Is the menu row visible to the role? `null` verb ⇒ any signed-in user. */
+function menuVisible(role: string, verb: string | null): boolean {
+  return verb === null ? true : hasVerb(grantsFor(role), verb);
+}
+
 function rolePriority(a: string, b: string): number {
   const order = ['viewer', 'maintainer', 'operator', 'admin'];
   const ia = order.indexOf(a);
@@ -301,6 +331,39 @@ function grantsOf(role: string): string[] {
         </div>
       </section>
 
+      <!-- Menu visibility matrix: which sidebar items each role sees.
+           Computed live from the policy via the same verb gates the
+           sidebar uses, so it stays honest if roles are reconfigured. -->
+      <section class="sw-card menu-matrix">
+        <header class="card-head">
+          <h3>Menu visibility</h3>
+          <span class="muted">which sidebar items each role sees · gated by 
the read verb in the last column (UI hides; the BFF enforces the same 
server-side)</span>
+        </header>
+        <div class="matrix-scroll">
+          <table class="matrix">
+            <thead>
+              <tr>
+                <th class="m-menu">Menu</th>
+                <th v-for="r in roleNames" :key="r" class="m-role">
+                  <span class="pill" :class="rolePill(r)">{{ r }}</span>
+                </th>
+                <th class="m-verb">Read verb</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="row in MENU_GATES" :key="row.label">
+                <td class="m-menu">{{ row.label }}</td>
+                <td v-for="r in roleNames" :key="r" class="m-cell">
+                  <span v-if="menuVisible(r, row.verb)" class="yes" 
title="visible">✔</span>
+                  <span v-else class="no" title="hidden">—</span>
+                </td>
+                <td class="m-verb"><code>{{ row.verb ?? 'any signed-in' 
}}</code></td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </section>
+
       <!-- Per-group cards: Menu scope (left) + Actions (right) -->
       <section v-for="g in groupedVerbs" :key="g.title" class="sw-card 
group-card">
         <header class="card-head">
@@ -610,4 +673,30 @@ function grantsOf(role: string): string[] {
 }
 .rule-k { font-size: 11.5px; font-weight: 600; color: var(--sw-fg-0); }
 .rule-v { font-size: 11.5px; color: var(--sw-fg-2); margin-top: 4px; 
line-height: 1.55; }
+
+.menu-matrix { margin-bottom: 14px; }
+.matrix-scroll { overflow-x: auto; }
+.matrix {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 11.5px;
+}
+.matrix th,
+.matrix td {
+  padding: 6px 10px;
+  border-bottom: 1px solid var(--sw-line);
+  text-align: center;
+  white-space: nowrap;
+}
+.matrix thead th { border-bottom: 2px solid var(--sw-line); }
+.matrix .m-menu { text-align: left; color: var(--sw-fg-0); }
+.matrix .m-verb { text-align: left; }
+.matrix .m-verb code {
+  font-family: var(--sw-mono);
+  font-size: 10.5px;
+  color: var(--sw-fg-3);
+}
+.matrix tbody tr:hover { background: var(--sw-bg-2); }
+.matrix .yes { color: var(--sw-ok, #34d399); font-weight: 700; }
+.matrix .no { color: var(--sw-fg-3); }
 </style>
diff --git a/horizon.example.yaml b/horizon.example.yaml
index 3e9b50a..224e90e 100644
--- a/horizon.example.yaml
+++ b/horizon.example.yaml
@@ -122,9 +122,9 @@ auth:
 rbac:
   enabled: true
   roles:
-    # Data catalog only. Deliberately NOT `*:read` so a viewer can't
-    # accidentally see rule definitions, live-debug sessions, setup
-    # screens, or platform internals.
+    # Data catalog + read-only overview dashboards. Deliberately NOT
+    # `*:read` so a viewer can't accidentally see rule definitions,
+    # live-debug sessions, setup screens, or platform internals.
     viewer:
       - metrics:read
       - alarms:read
@@ -132,6 +132,7 @@ rbac:
       - logs:read
       - topology:read
       - profile:read
+      - overview:read
 
     # Viewer + platform monitoring (OAP cluster + module inspector).
     maintainer:
@@ -141,6 +142,7 @@ rbac:
       - logs:read
       - topology:read
       - profile:read
+      - overview:read
       - cluster:read
       - inspect:read
 

Reply via email to