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(),