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 188ecac  layers: JSON template config per layer (alias + slots + 
components + metrics + widgets)
188ecac is described below

commit 188ecac03ebf15527c4e1c95de833691ffd32051
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 12 20:33:27 2026 +0800

    layers: JSON template config per layer (alias + slots + components + 
metrics + widgets)
    
    Move the hardcoded layer defaults out of TS source into one JSON file
    per OAP layer enum under apps/bff/src/layers/config/<key>.json. Each
    template carries:
      - alias / color / documentLink — header identity
      - slots — service/instance/endpoint alias terms
      - components — which per-layer pages exist (service / instances /
        endpoints / topology / traces / logs / profiling)
      - metrics.columns — landing card columns with MQE + aggregation
      - widgets — dashboard widget set with MQE expressions + grid coords
    
    Bundled this commit: general, mesh, k8s_service, virtual_database,
    virtual_cache, virtual_mq. Other layers fall through to the existing
    TS fallbacks until templates are added.
    
    BFF wiring:
    - New apps/bff/src/layers/loader.ts reads JSONs at startup, caches in
      memory, exposes getLayerTemplate / allLayerTemplates / reload.
    - menu-routes.deriveLayer prefers template alias / color / slots /
      caps over the LAYER_DEFAULTS table when a template exists.
    - dashboard/defaults.defaultWidgetsFor prefers template widgets over
      the hardcoded SERVICE_WIDGETS / BROWSER_WIDGETS sets.
    
    This is the substrate the upcoming admin/layer-dashboards page edits.
---
 apps/bff/src/dashboard/defaults.ts               |  15 ++-
 apps/bff/src/layers/config/general.json          | 101 ++++++++++++++++
 apps/bff/src/layers/config/k8s_service.json      |  65 +++++++++++
 apps/bff/src/layers/config/mesh.json             |  76 ++++++++++++
 apps/bff/src/layers/config/virtual_cache.json    |  44 +++++++
 apps/bff/src/layers/config/virtual_database.json |  56 +++++++++
 apps/bff/src/layers/config/virtual_mq.json       |  44 +++++++
 apps/bff/src/layers/loader.ts                    | 141 +++++++++++++++++++++++
 apps/bff/src/oap/menu-routes.ts                  |  38 ++++++
 9 files changed, 574 insertions(+), 6 deletions(-)

diff --git a/apps/bff/src/dashboard/defaults.ts 
b/apps/bff/src/dashboard/defaults.ts
index e62a38d..01c7797 100644
--- a/apps/bff/src/dashboard/defaults.ts
+++ b/apps/bff/src/dashboard/defaults.ts
@@ -27,6 +27,7 @@
  */
 
 import type { DashboardWidget } from '@skywalking-horizon-ui/api-client';
+import { getLayerTemplate } from '../layers/loader.js';
 
 /** Service-scope service-shaped layers (general / mesh / k8s_service). */
 const SERVICE_WIDGETS: DashboardWidget[] = [
@@ -178,15 +179,17 @@ const GENERIC_WIDGETS: DashboardWidget[] = [
 ];
 
 /**
- * Resolve the default widget set for `(layerKey)`. The set is per
- * layer enum, but we also map alias keys (CACHE → VIRTUAL_CACHE etc.)
- * to the modern enum since older OAP builds still emit aliases.
+ * Resolve the default widget set for `(layerKey)`. First tries the
+ * JSON layer template (`src/layers/config/<key>.json`); falls back to
+ * the hardcoded TS sets above when no JSON exists for the layer. JSON
+ * wins because that's where operators will eventually edit widgets
+ * via the admin page.
  */
 export function defaultWidgetsFor(layerKey: string): DashboardWidget[] {
+  const tpl = getLayerTemplate(layerKey);
+  if (tpl && tpl.widgets && tpl.widgets.length > 0) return tpl.widgets;
   const k = layerKey.toUpperCase();
-  if (k === 'GENERAL' || k === 'MESH' || k === 'MESH_CP' || k === 'MESH_DP' || 
k === 'K8S_SERVICE') {
-    return SERVICE_WIDGETS;
-  }
+  if (k === 'MESH_CP' || k === 'MESH_DP') return SERVICE_WIDGETS;
   if (k === 'BROWSER') return BROWSER_WIDGETS;
   return GENERIC_WIDGETS;
 }
diff --git a/apps/bff/src/layers/config/general.json 
b/apps/bff/src/layers/config/general.json
new file mode 100644
index 0000000..f932be7
--- /dev/null
+++ b/apps/bff/src/layers/config/general.json
@@ -0,0 +1,101 @@
+{
+  "key": "GENERAL",
+  "alias": "General Service",
+  "color": "var(--sw-accent)",
+  "documentLink": 
"https://skywalking.apache.org/docs/main/latest/en/setup/service-agent/";,
+  "slots": {
+    "services": "Services",
+    "instances": "Instances",
+    "endpoints": "API",
+    "endpointDependency": "API dependency"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": true,
+    "endpointDependency": true,
+    "topology": true,
+    "traces": true,
+    "logs": true,
+    "profiling": true
+  },
+  "metrics": {
+    "throughput": "cpm",
+    "spark": "cpm",
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "Traffic", "unit": "rpm", "mqe": 
"service_cpm", "aggregation": "sum" },
+      { "metric": "p99", "label": "p99", "unit": "ms", "mqe": 
"service_percentile{p='99'}", "aggregation": "avg" },
+      { "metric": "sla", "label": "SLA", "unit": "%", "mqe": 
"service_sla/100", "aggregation": "avg" },
+      { "metric": "err", "label": "err", "unit": "%", "mqe": "100 - 
service_sla/100", "aggregation": "avg" }
+    ]
+  },
+  "widgets": [
+    {
+      "id": "apdex",
+      "title": "Apdex",
+      "tip": "User satisfaction score on a 0–1 scale. service_apdex is 
integer-times-10000 on the server side; the expression scales it.",
+      "type": "card",
+      "expressions": ["avg(service_apdex)/10000"],
+      "x": 0, "y": 0, "w": 8, "h": 5
+    },
+    {
+      "id": "sla",
+      "title": "Success Rate",
+      "tip": "Percentage of successful requests.",
+      "type": "card",
+      "unit": "%",
+      "expressions": ["avg(service_sla)/100"],
+      "x": 8, "y": 0, "w": 8, "h": 5
+    },
+    {
+      "id": "traffic",
+      "title": "Traffic",
+      "tip": "Requests per minute. For HTTP / gRPC / RPC services this 
reflects request throughput.",
+      "type": "card",
+      "unit": "rpm",
+      "expressions": ["avg(service_cpm)"],
+      "x": 16, "y": 0, "w": 8, "h": 5
+    },
+    {
+      "id": "resp_time",
+      "title": "Avg Response Time",
+      "tip": "Mean latency across all calls in the window.",
+      "type": "line",
+      "unit": "ms",
+      "expressions": ["service_resp_time"],
+      "x": 0, "y": 5, "w": 12, "h": 14
+    },
+    {
+      "id": "percentile",
+      "title": "Response Time Percentile",
+      "tip": "Latency at p50 / p75 / p90 / p95 / p99 — useful for tail 
behavior.",
+      "type": "line",
+      "unit": "ms",
+      "expressions": [
+        "service_percentile{p='50'}",
+        "service_percentile{p='75'}",
+        "service_percentile{p='90'}",
+        "service_percentile{p='95'}",
+        "service_percentile{p='99'}"
+      ],
+      "x": 12, "y": 5, "w": 12, "h": 14
+    },
+    {
+      "id": "traffic_line",
+      "title": "Traffic",
+      "type": "line",
+      "unit": "rpm",
+      "expressions": ["service_cpm"],
+      "x": 0, "y": 19, "w": 12, "h": 12
+    },
+    {
+      "id": "sla_line",
+      "title": "Success Rate",
+      "type": "line",
+      "unit": "%",
+      "expressions": ["service_sla/100"],
+      "x": 12, "y": 19, "w": 12, "h": 12
+    }
+  ]
+}
diff --git a/apps/bff/src/layers/config/k8s_service.json 
b/apps/bff/src/layers/config/k8s_service.json
new file mode 100644
index 0000000..875e220
--- /dev/null
+++ b/apps/bff/src/layers/config/k8s_service.json
@@ -0,0 +1,65 @@
+{
+  "key": "K8S_SERVICE",
+  "alias": "K8s Service",
+  "color": "var(--sw-purple)",
+  "slots": {
+    "services": "K8s services",
+    "instances": "Pods"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "topology": true,
+    "traces": true,
+    "logs": true
+  },
+  "metrics": {
+    "throughput": "cpm",
+    "spark": "cpm",
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "Traffic", "unit": "rpm", "mqe": 
"service_cpm", "aggregation": "sum" },
+      { "metric": "p99", "label": "p99", "unit": "ms", "mqe": 
"service_percentile{p='99'}", "aggregation": "avg" },
+      { "metric": "sla", "label": "SLA", "unit": "%", "mqe": 
"service_sla/100", "aggregation": "avg" },
+      { "metric": "err", "label": "err", "unit": "%", "mqe": "100 - 
service_sla/100", "aggregation": "avg" }
+    ]
+  },
+  "widgets": [
+    {
+      "id": "traffic",
+      "title": "Traffic",
+      "type": "card",
+      "unit": "rpm",
+      "expressions": ["avg(service_cpm)"],
+      "x": 0, "y": 0, "w": 12, "h": 5
+    },
+    {
+      "id": "sla",
+      "title": "Success Rate",
+      "type": "card",
+      "unit": "%",
+      "expressions": ["avg(service_sla)/100"],
+      "x": 12, "y": 0, "w": 12, "h": 5
+    },
+    {
+      "id": "traffic_line",
+      "title": "Traffic",
+      "type": "line",
+      "unit": "rpm",
+      "expressions": ["service_cpm"],
+      "x": 0, "y": 5, "w": 12, "h": 12
+    },
+    {
+      "id": "percentile",
+      "title": "Response Time Percentile",
+      "type": "line",
+      "unit": "ms",
+      "expressions": [
+        "service_percentile{p='50'}",
+        "service_percentile{p='95'}",
+        "service_percentile{p='99'}"
+      ],
+      "x": 12, "y": 5, "w": 12, "h": 12
+    }
+  ]
+}
diff --git a/apps/bff/src/layers/config/mesh.json 
b/apps/bff/src/layers/config/mesh.json
new file mode 100644
index 0000000..47c6cef
--- /dev/null
+++ b/apps/bff/src/layers/config/mesh.json
@@ -0,0 +1,76 @@
+{
+  "key": "MESH",
+  "alias": "Service Mesh",
+  "color": "var(--sw-info)",
+  "slots": {
+    "services": "Services",
+    "instances": "Sidecars",
+    "endpoints": "Endpoints"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": true,
+    "endpointDependency": true,
+    "topology": true,
+    "traces": true,
+    "logs": true
+  },
+  "metrics": {
+    "throughput": "cpm",
+    "spark": "cpm",
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "Traffic", "unit": "rpm", "mqe": 
"service_cpm", "aggregation": "sum" },
+      { "metric": "p99", "label": "p99", "unit": "ms", "mqe": 
"service_percentile{p='99'}", "aggregation": "avg" },
+      { "metric": "sla", "label": "SLA", "unit": "%", "mqe": 
"service_sla/100", "aggregation": "avg" },
+      { "metric": "err", "label": "err", "unit": "%", "mqe": "100 - 
service_sla/100", "aggregation": "avg" }
+    ]
+  },
+  "widgets": [
+    {
+      "id": "sla",
+      "title": "Success Rate",
+      "type": "card",
+      "unit": "%",
+      "expressions": ["avg(service_sla)/100"],
+      "x": 0, "y": 0, "w": 8, "h": 5
+    },
+    {
+      "id": "traffic",
+      "title": "Traffic",
+      "type": "card",
+      "unit": "rpm",
+      "expressions": ["avg(service_cpm)"],
+      "x": 8, "y": 0, "w": 8, "h": 5
+    },
+    {
+      "id": "p99",
+      "title": "p99 latency",
+      "type": "card",
+      "unit": "ms",
+      "expressions": ["service_percentile{p='99'}"],
+      "x": 16, "y": 0, "w": 8, "h": 5
+    },
+    {
+      "id": "traffic_line",
+      "title": "Traffic",
+      "type": "line",
+      "unit": "rpm",
+      "expressions": ["service_cpm"],
+      "x": 0, "y": 5, "w": 12, "h": 12
+    },
+    {
+      "id": "percentile",
+      "title": "Response Time Percentile",
+      "type": "line",
+      "unit": "ms",
+      "expressions": [
+        "service_percentile{p='50'}",
+        "service_percentile{p='95'}",
+        "service_percentile{p='99'}"
+      ],
+      "x": 12, "y": 5, "w": 12, "h": 12
+    }
+  ]
+}
diff --git a/apps/bff/src/layers/config/virtual_cache.json 
b/apps/bff/src/layers/config/virtual_cache.json
new file mode 100644
index 0000000..048535e
--- /dev/null
+++ b/apps/bff/src/layers/config/virtual_cache.json
@@ -0,0 +1,44 @@
+{
+  "key": "VIRTUAL_CACHE",
+  "alias": "Virtual Cache",
+  "color": "var(--sw-warn)",
+  "slots": {
+    "services": "Caches"
+  },
+  "components": { "service": true },
+  "metrics": {
+    "throughput": "cpm",
+    "spark": "cpm",
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "Traffic", "unit": "rpm", "mqe": 
"service_cpm", "aggregation": "sum" },
+      { "metric": "resp", "label": "resp", "unit": "ms", "mqe": 
"service_resp_time", "aggregation": "avg" }
+    ]
+  },
+  "widgets": [
+    {
+      "id": "traffic",
+      "title": "Traffic",
+      "type": "card",
+      "unit": "rpm",
+      "expressions": ["avg(service_cpm)"],
+      "x": 0, "y": 0, "w": 12, "h": 5
+    },
+    {
+      "id": "resp",
+      "title": "Avg Response Time",
+      "type": "card",
+      "unit": "ms",
+      "expressions": ["avg(service_resp_time)"],
+      "x": 12, "y": 0, "w": 12, "h": 5
+    },
+    {
+      "id": "traffic_line",
+      "title": "Traffic",
+      "type": "line",
+      "unit": "rpm",
+      "expressions": ["service_cpm"],
+      "x": 0, "y": 5, "w": 24, "h": 12
+    }
+  ]
+}
diff --git a/apps/bff/src/layers/config/virtual_database.json 
b/apps/bff/src/layers/config/virtual_database.json
new file mode 100644
index 0000000..0b186b4
--- /dev/null
+++ b/apps/bff/src/layers/config/virtual_database.json
@@ -0,0 +1,56 @@
+{
+  "key": "VIRTUAL_DATABASE",
+  "alias": "Virtual Database",
+  "color": "var(--sw-warn)",
+  "slots": {
+    "services": "Databases"
+  },
+  "components": {
+    "service": true
+  },
+  "metrics": {
+    "throughput": "cpm",
+    "spark": "cpm",
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "Traffic", "unit": "rpm", "mqe": 
"service_cpm", "aggregation": "sum" },
+      { "metric": "resp", "label": "resp", "unit": "ms", "mqe": 
"service_resp_time", "aggregation": "avg" },
+      { "metric": "p99", "label": "p99", "unit": "ms", "mqe": 
"service_percentile{p='99'}", "aggregation": "avg" },
+      { "metric": "err", "label": "err", "unit": "%", "mqe": "100 - 
service_sla/100", "aggregation": "avg" }
+    ]
+  },
+  "widgets": [
+    {
+      "id": "traffic",
+      "title": "Traffic",
+      "type": "card",
+      "unit": "rpm",
+      "expressions": ["avg(service_cpm)"],
+      "x": 0, "y": 0, "w": 8, "h": 5
+    },
+    {
+      "id": "resp",
+      "title": "Avg Response Time",
+      "type": "card",
+      "unit": "ms",
+      "expressions": ["avg(service_resp_time)"],
+      "x": 8, "y": 0, "w": 8, "h": 5
+    },
+    {
+      "id": "sla",
+      "title": "Success Rate",
+      "type": "card",
+      "unit": "%",
+      "expressions": ["avg(service_sla)/100"],
+      "x": 16, "y": 0, "w": 8, "h": 5
+    },
+    {
+      "id": "traffic_line",
+      "title": "Traffic",
+      "type": "line",
+      "unit": "rpm",
+      "expressions": ["service_cpm"],
+      "x": 0, "y": 5, "w": 24, "h": 12
+    }
+  ]
+}
diff --git a/apps/bff/src/layers/config/virtual_mq.json 
b/apps/bff/src/layers/config/virtual_mq.json
new file mode 100644
index 0000000..5bd061b
--- /dev/null
+++ b/apps/bff/src/layers/config/virtual_mq.json
@@ -0,0 +1,44 @@
+{
+  "key": "VIRTUAL_MQ",
+  "alias": "Virtual MQ",
+  "color": "var(--sw-ok)",
+  "slots": {
+    "services": "Queues"
+  },
+  "components": { "service": true },
+  "metrics": {
+    "throughput": "cpm",
+    "spark": "cpm",
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "msg/s", "mqe": "service_cpm", 
"aggregation": "sum" },
+      { "metric": "resp", "label": "consume", "unit": "ms", "mqe": 
"service_resp_time", "aggregation": "avg" }
+    ]
+  },
+  "widgets": [
+    {
+      "id": "traffic",
+      "title": "Message rate",
+      "type": "card",
+      "unit": "msg / min",
+      "expressions": ["avg(service_cpm)"],
+      "x": 0, "y": 0, "w": 12, "h": 5
+    },
+    {
+      "id": "resp",
+      "title": "Avg Consume Latency",
+      "type": "card",
+      "unit": "ms",
+      "expressions": ["avg(service_resp_time)"],
+      "x": 12, "y": 0, "w": 12, "h": 5
+    },
+    {
+      "id": "traffic_line",
+      "title": "Message rate",
+      "type": "line",
+      "unit": "msg / min",
+      "expressions": ["service_cpm"],
+      "x": 0, "y": 5, "w": 24, "h": 12
+    }
+  ]
+}
diff --git a/apps/bff/src/layers/loader.ts b/apps/bff/src/layers/loader.ts
new file mode 100644
index 0000000..6627752
--- /dev/null
+++ b/apps/bff/src/layers/loader.ts
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Per-layer template config — the source of truth for a layer's
+ * presentation (alias, color, slots, enabled components) AND its data
+ * shape (landing card columns, dashboard widgets, MQE expressions).
+ *
+ * The bundled defaults live under `./config/<key>.json`, one file per
+ * OAP layer enum. Operator overrides land in the SetupStore (JSON file
+ * on disk) and merge on top.
+ *
+ * Lifting these from TS code into JSON gets us:
+ *   - One file per layer to review or copy
+ *   - Operator-editable surface (the future admin/layer-dashboards page
+ *     reads + writes JSON-shaped configs)
+ *   - Clean separation between code (the loader) and content (the
+ *     widget catalog)
+ */
+
+import { readdirSync, readFileSync } from 'node:fs';
+import { dirname, join, basename } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import type { DashboardWidget } from '@skywalking-horizon-ui/api-client';
+
+export interface LayerComponentFlags {
+  service?: boolean;
+  instances?: boolean;
+  endpoints?: boolean;
+  endpointDependency?: boolean;
+  topology?: boolean;
+  traces?: boolean;
+  logs?: boolean;
+  profiling?: boolean;
+}
+
+export interface LayerSlotsConfig {
+  services?: string;
+  instances?: string;
+  endpoints?: string;
+  endpointDependency?: string;
+}
+
+export interface LayerMetricColumn {
+  metric: string;
+  label: string;
+  unit?: string;
+  mqe?: string;
+  aggregation?: 'sum' | 'avg';
+  scale?: number;
+  precision?: number;
+}
+
+export interface LayerMetricsConfig {
+  /** Default order-by metric key for the topN service ranking. */
+  orderBy?: string;
+  /** Throughput metric key (drives the per-layer KPI tile + spark). */
+  throughput?: string;
+  /** Spark metric key (defaults to `throughput` when omitted). */
+  spark?: string;
+  columns?: LayerMetricColumn[];
+}
+
+export interface LayerTemplate {
+  /** UPPER_SNAKE enum key (matches OAP). */
+  key: string;
+  /** Display name override. */
+  alias?: string;
+  /** Layer-dot color (CSS var or hex). */
+  color?: string;
+  /** Doc link surfaced as a chip on the layer page. */
+  documentLink?: string;
+  slots: LayerSlotsConfig;
+  components: LayerComponentFlags;
+  metrics: LayerMetricsConfig;
+  widgets: DashboardWidget[];
+}
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const CONFIG_DIR = join(__dirname, 'config');
+
+let cache: Map<string, LayerTemplate> | null = null;
+
+function load(): Map<string, LayerTemplate> {
+  const out = new Map<string, LayerTemplate>();
+  for (const file of readdirSync(CONFIG_DIR)) {
+    if (!file.endsWith('.json')) continue;
+    const raw = readFileSync(join(CONFIG_DIR, file), 'utf-8');
+    let parsed: LayerTemplate;
+    try {
+      parsed = JSON.parse(raw) as LayerTemplate;
+    } catch (err) {
+      throw new Error(`failed to parse layer config ${file}: ${err instanceof 
Error ? err.message : err}`);
+    }
+    // File name should match the layer key (case-insensitive) so the
+    // operator can rename / locate the right file without grepping JSON.
+    const expected = basename(file, '.json').toUpperCase();
+    if (parsed.key && parsed.key.toUpperCase() !== expected) {
+      throw new Error(
+        `layer config ${file}: file basename does not match \`key\` 
(${parsed.key})`,
+      );
+    }
+    out.set(parsed.key.toUpperCase(), parsed);
+  }
+  return out;
+}
+
+/**
+ * Lookup a layer template by enum key (case-insensitive). Returns
+ * `null` when no template is defined for the layer — call sites should
+ * fall back to a generic shape rather than failing.
+ */
+export function getLayerTemplate(layerKey: string): LayerTemplate | null {
+  if (!cache) cache = load();
+  return cache.get(layerKey.toUpperCase()) ?? null;
+}
+
+/** All loaded templates, useful for the admin layer-dashboards page. */
+export function allLayerTemplates(): LayerTemplate[] {
+  if (!cache) cache = load();
+  return Array.from(cache.values());
+}
+
+/** Force a reload from disk — used by a future admin save endpoint. */
+export function reloadLayerTemplates(): void {
+  cache = null;
+}
diff --git a/apps/bff/src/oap/menu-routes.ts b/apps/bff/src/oap/menu-routes.ts
index 6f157fe..13f745d 100644
--- a/apps/bff/src/oap/menu-routes.ts
+++ b/apps/bff/src/oap/menu-routes.ts
@@ -27,6 +27,27 @@ import type { ConfigSource } from '../config/loader.js';
 import type { SessionStore } from '../auth/sessions.js';
 import { requireAuth } from '../auth/middleware.js';
 import { graphqlPost } from './graphql-client.js';
+import { getLayerTemplate, type LayerComponentFlags } from 
'../layers/loader.js';
+
+/**
+ * Map the JSON config's `components.*` flags onto the wire `caps`
+ * shape — caps are the cap-driven feature toggles each per-layer page
+ * consults. We expand a few aliases (service ⇒ no separate cap; the
+ * components flag is the source of truth for whether the page exists).
+ */
+function componentsToCaps(components: LayerComponentFlags): LayerCaps {
+  return {
+    dashboards: components.service !== false,
+    endpointDependency: !!components.endpointDependency,
+    serviceMap: !!components.topology,
+    instanceTopology: !!components.topology,
+    processTopology: !!components.topology,
+    traces: !!components.traces,
+    logs: !!components.logs,
+    profiling: !!components.profiling,
+    events: false,
+  };
+}
 
 export interface MenuRouteDeps {
   config: ConfigSource;
@@ -143,6 +164,23 @@ function deriveLayer(
   items: MenuRaw['items'],
 ): LayerDef {
   const item = items.find((i) => canonical(i.layer) === rawKey);
+  // JSON template wins when present — alias / color / slots / caps all
+  // come from there. Hardcoded LAYER_DEFAULTS stays as the fallback for
+  // layers without a template (older OAP layers, custom layers).
+  const tpl = getLayerTemplate(rawKey);
+  if (tpl) {
+    return {
+      key: rawKey.toLowerCase(),
+      name: tpl.alias || item?.title?.trim() || rawKey,
+      color: tpl.color || 'var(--sw-fg-2)',
+      serviceCount,
+      active,
+      level,
+      documentLink: tpl.documentLink ?? item?.documentLink ?? undefined,
+      slots: tpl.slots,
+      caps: componentsToCaps(tpl.components),
+    };
+  }
   const def = LAYER_DEFAULTS[rawKey] ?? DEFAULT_FOR_UNKNOWN_LAYER;
   return {
     key: rawKey.toLowerCase(),

Reply via email to