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 eaebf40 config: split metrics/overview blocks; admin gains separate
Overview tile card
eaebf40 is described below
commit eaebf401241ad102db64ad93984f849195e9e004
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 12 22:48:16 2026 +0800
config: split metrics/overview blocks; admin gains separate Overview tile
card
Schema split: the layer template's metrics block used to bundle the
service-list config (orderBy, columns) with Overview-page settings
(throughput, spark). Two pages, two concerns — they now live in
separate blocks:
'metrics': orderBy + columns ← drives the service list
'overview': throughput + spark ← drives the Overview tile
Loader migrates legacy JSONs at load: metrics.throughput / .spark
move to overview.throughput / .spark transparently, so old templates
don't break.
All five bundled layer JSONs (general / mesh / k8s_service /
virtual_database / virtual_cache / virtual_mq) restructured to the
new shape — slots also renamed to aliases everywhere.
UI wire types + setup store updated:
- LayerDef gains optional .overview alongside .metrics.
- defaultLandingFor accepts both blocks; reads throughput/spark
from overview when present.
- ensure() callers (LayerShell, LayerKpiStripCard, LayerKpiTile,
LayerLandingCard, LayerDashboardsView, LayerSetupCard, SetupView,
useLandingOrder) forward layer.overview through.
Admin / Layer dashboards UI split:
- 'Service list metrics' card: orderBy dropdown + columns editor.
- 'Overview tile' card (new): throughput + spark dropdowns.
Both reference the same columns array — operators can't pick a
metric that isn't a defined column.
BFF admin-save zod schema accepts the new shape (overview block
optional; legacy metrics.throughput / .spark rejected, but loader
still migrates incoming JSON files at startup).
---
apps/bff/src/dashboard/routes.ts | 9 ++-
apps/bff/src/layers/config/general.json | 6 +-
apps/bff/src/layers/config/k8s_service.json | 72 ++++++++++++++----
apps/bff/src/layers/config/mesh.json | 81 +++++++++++++++++----
apps/bff/src/layers/config/virtual_cache.json | 55 +++++++++++---
apps/bff/src/layers/config/virtual_database.json | 76 +++++++++++++++----
apps/bff/src/layers/config/virtual_mq.json | 54 +++++++++++---
apps/bff/src/layers/loader.ts | 36 ++++++++-
apps/bff/src/oap/menu-routes.ts | 1 +
apps/ui/src/api/client.ts | 6 +-
apps/ui/src/composables/useLandingOrder.ts | 4 +-
apps/ui/src/stores/setup.ts | 34 +++++++--
apps/ui/src/views/admin/LayerDashboardsAdmin.vue | 93 +++++++++++++++++-------
apps/ui/src/views/layer/LayerDashboardsView.vue | 2 +-
apps/ui/src/views/layer/LayerShell.vue | 2 +-
apps/ui/src/views/overview/LayerKpiStripCard.vue | 2 +-
apps/ui/src/views/overview/LayerKpiTile.vue | 2 +-
apps/ui/src/views/overview/LayerLandingCard.vue | 2 +-
apps/ui/src/views/setup/LayerSetupCard.vue | 2 +-
apps/ui/src/views/setup/SetupView.vue | 2 +-
packages/api-client/src/index.ts | 1 +
packages/api-client/src/menu.ts | 33 +++++++--
22 files changed, 447 insertions(+), 128 deletions(-)
diff --git a/apps/bff/src/dashboard/routes.ts b/apps/bff/src/dashboard/routes.ts
index 9e005a7..778b5c9 100644
--- a/apps/bff/src/dashboard/routes.ts
+++ b/apps/bff/src/dashboard/routes.ts
@@ -530,8 +530,6 @@ export function registerDashboardRoute(app:
FastifyInstance, deps: DashboardRout
metrics: z
.object({
orderBy: z.string().optional(),
- throughput: z.string().optional(),
- spark: z.string().optional(),
columns: z
.array(
z.object({
@@ -548,6 +546,13 @@ export function registerDashboardRoute(app:
FastifyInstance, deps: DashboardRout
.optional(),
})
.strict(),
+ overview: z
+ .object({
+ throughput: z.string().optional(),
+ spark: z.string().optional(),
+ })
+ .strict()
+ .optional(),
dashboards: z
.object({
service: z.array(widgetSchema).max(40).optional(),
diff --git a/apps/bff/src/layers/config/general.json
b/apps/bff/src/layers/config/general.json
index 07911c3..38b6189 100644
--- a/apps/bff/src/layers/config/general.json
+++ b/apps/bff/src/layers/config/general.json
@@ -20,8 +20,6 @@
"profiling": true
},
"metrics": {
- "throughput": "cpm",
- "spark": "cpm",
"orderBy": "cpm",
"columns": [
{ "metric": "cpm", "label": "RPM", "mqe": "service_cpm", "aggregation":
"sum" },
@@ -29,6 +27,10 @@
{ "metric": "err", "label": "Error Rate", "unit": "%", "mqe": "100 -
service_sla/100", "aggregation": "avg" }
]
},
+ "overview": {
+ "throughput": "cpm",
+ "spark": "cpm"
+ },
"dashboards": {
"service": [
{
diff --git a/apps/bff/src/layers/config/k8s_service.json
b/apps/bff/src/layers/config/k8s_service.json
index 875e220..0ea174d 100644
--- a/apps/bff/src/layers/config/k8s_service.json
+++ b/apps/bff/src/layers/config/k8s_service.json
@@ -2,7 +2,7 @@
"key": "K8S_SERVICE",
"alias": "K8s Service",
"color": "var(--sw-purple)",
- "slots": {
+ "aliases": {
"services": "K8s services",
"instances": "Pods"
},
@@ -14,40 +14,81 @@
"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" }
+ {
+ "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"
+ }
]
},
+ "overview": {
+ "throughput": "cpm",
+ "spark": "cpm"
+ },
"widgets": [
{
"id": "traffic",
"title": "Traffic",
"type": "card",
"unit": "rpm",
- "expressions": ["avg(service_cpm)"],
- "x": 0, "y": 0, "w": 12, "h": 5
+ "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
+ "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
+ "expressions": [
+ "service_cpm"
+ ],
+ "x": 0,
+ "y": 5,
+ "w": 12,
+ "h": 12
},
{
"id": "percentile",
@@ -59,7 +100,10 @@
"service_percentile{p='95'}",
"service_percentile{p='99'}"
],
- "x": 12, "y": 5, "w": 12, "h": 12
+ "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
index 47c6cef..dffd4ff 100644
--- a/apps/bff/src/layers/config/mesh.json
+++ b/apps/bff/src/layers/config/mesh.json
@@ -2,7 +2,7 @@
"key": "MESH",
"alias": "Service Mesh",
"color": "var(--sw-info)",
- "slots": {
+ "aliases": {
"services": "Services",
"instances": "Sidecars",
"endpoints": "Endpoints"
@@ -17,48 +17,94 @@
"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" }
+ {
+ "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"
+ }
]
},
+ "overview": {
+ "throughput": "cpm",
+ "spark": "cpm"
+ },
"widgets": [
{
"id": "sla",
"title": "Success Rate",
"type": "card",
"unit": "%",
- "expressions": ["avg(service_sla)/100"],
- "x": 0, "y": 0, "w": 8, "h": 5
+ "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
+ "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
+ "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
+ "expressions": [
+ "service_cpm"
+ ],
+ "x": 0,
+ "y": 5,
+ "w": 12,
+ "h": 12
},
{
"id": "percentile",
@@ -70,7 +116,10 @@
"service_percentile{p='95'}",
"service_percentile{p='99'}"
],
- "x": 12, "y": 5, "w": 12, "h": 12
+ "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
index 048535e..a6c3bde 100644
--- a/apps/bff/src/layers/config/virtual_cache.json
+++ b/apps/bff/src/layers/config/virtual_cache.json
@@ -2,43 +2,74 @@
"key": "VIRTUAL_CACHE",
"alias": "Virtual Cache",
"color": "var(--sw-warn)",
- "slots": {
+ "aliases": {
"services": "Caches"
},
- "components": { "service": true },
+ "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": "cpm",
+ "label": "Traffic",
+ "unit": "rpm",
+ "mqe": "service_cpm",
+ "aggregation": "sum"
+ },
+ {
+ "metric": "resp",
+ "label": "resp",
+ "unit": "ms",
+ "mqe": "service_resp_time",
+ "aggregation": "avg"
+ }
]
},
+ "overview": {
+ "throughput": "cpm",
+ "spark": "cpm"
+ },
"widgets": [
{
"id": "traffic",
"title": "Traffic",
"type": "card",
"unit": "rpm",
- "expressions": ["avg(service_cpm)"],
- "x": 0, "y": 0, "w": 12, "h": 5
+ "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
+ "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
+ "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
index 0b186b4..cf16f42 100644
--- a/apps/bff/src/layers/config/virtual_database.json
+++ b/apps/bff/src/layers/config/virtual_database.json
@@ -2,55 +2,101 @@
"key": "VIRTUAL_DATABASE",
"alias": "Virtual Database",
"color": "var(--sw-warn)",
- "slots": {
+ "aliases": {
"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" }
+ {
+ "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"
+ }
]
},
+ "overview": {
+ "throughput": "cpm",
+ "spark": "cpm"
+ },
"widgets": [
{
"id": "traffic",
"title": "Traffic",
"type": "card",
"unit": "rpm",
- "expressions": ["avg(service_cpm)"],
- "x": 0, "y": 0, "w": 8, "h": 5
+ "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
+ "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
+ "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
+ "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
index 5bd061b..21bef7c 100644
--- a/apps/bff/src/layers/config/virtual_mq.json
+++ b/apps/bff/src/layers/config/virtual_mq.json
@@ -2,43 +2,73 @@
"key": "VIRTUAL_MQ",
"alias": "Virtual MQ",
"color": "var(--sw-ok)",
- "slots": {
+ "aliases": {
"services": "Queues"
},
- "components": { "service": true },
+ "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" }
+ {
+ "metric": "cpm",
+ "label": "msg/s",
+ "mqe": "service_cpm",
+ "aggregation": "sum"
+ },
+ {
+ "metric": "resp",
+ "label": "consume",
+ "unit": "ms",
+ "mqe": "service_resp_time",
+ "aggregation": "avg"
+ }
]
},
+ "overview": {
+ "throughput": "cpm",
+ "spark": "cpm"
+ },
"widgets": [
{
"id": "traffic",
"title": "Message rate",
"type": "card",
"unit": "msg / min",
- "expressions": ["avg(service_cpm)"],
- "x": 0, "y": 0, "w": 12, "h": 5
+ "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
+ "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
+ "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
index bf69aa0..4a8b93d 100644
--- a/apps/bff/src/layers/loader.ts
+++ b/apps/bff/src/layers/loader.ts
@@ -66,13 +66,22 @@ export interface LayerMetricColumn {
}
export interface LayerMetricsConfig {
- /** Default order-by metric key for the topN service ranking. */
+ /** Default sort metric for the service list. */
orderBy?: string;
- /** Throughput metric key (drives the per-layer KPI tile + spark). */
+ columns?: LayerMetricColumn[];
+}
+
+/**
+ * Overview-tile-only settings. Headline + trend metrics on the
+ * per-layer compact tile in the Overview's top strip. Separated
+ * from `LayerMetricsConfig` because these settings affect ONLY the
+ * Overview page; the service list / per-layer pages don't read them.
+ */
+export interface LayerOverviewConfig {
+ /** Metric key for the Overview tile's big headline value. */
throughput?: string;
- /** Spark metric key (defaults to `throughput` when omitted). */
+ /** Metric key for the Overview tile's trend line. */
spark?: string;
- columns?: LayerMetricColumn[];
}
/**
@@ -102,6 +111,10 @@ export interface LayerTemplate {
slots: LayerSlotsConfig;
components: LayerComponentFlags;
metrics: LayerMetricsConfig;
+ /** Overview-tile only — headline + trend metrics for the Overview's
+ * per-layer compact tile. Optional; falls back to `metrics.orderBy`
+ * for the headline when omitted. */
+ overview?: LayerOverviewConfig;
/** Per-scope widget sets. `service` is the layer's primary landing. */
dashboards?: LayerDashboards;
/** Legacy single widget list — treated as `dashboards.service`. */
@@ -145,6 +158,21 @@ function load(): Map<string, LayerTemplate> {
if (parsed.widgets && (!parsed.dashboards || !parsed.dashboards.service)) {
parsed.dashboards = { ...parsed.dashboards, service: parsed.widgets };
}
+ // Migrate legacy `metrics.throughput` + `metrics.spark` → top-level
+ // `overview` block. Overview-page settings used to live alongside
+ // the service-list metric config; they're now split since they
+ // affect different pages.
+ const legacyMetrics = parsed.metrics as
+ | (LayerMetricsConfig & { throughput?: string; spark?: string })
+ | undefined;
+ if (legacyMetrics && (legacyMetrics.throughput || legacyMetrics.spark)) {
+ const ov: LayerOverviewConfig = { ...(parsed.overview ?? {}) };
+ if (!ov.throughput && legacyMetrics.throughput) ov.throughput =
legacyMetrics.throughput;
+ if (!ov.spark && legacyMetrics.spark) ov.spark = legacyMetrics.spark;
+ parsed.overview = ov;
+ delete legacyMetrics.throughput;
+ delete legacyMetrics.spark;
+ }
out.set(parsed.key.toUpperCase(), parsed);
}
return out;
diff --git a/apps/bff/src/oap/menu-routes.ts b/apps/bff/src/oap/menu-routes.ts
index 93914d0..80389cf 100644
--- a/apps/bff/src/oap/menu-routes.ts
+++ b/apps/bff/src/oap/menu-routes.ts
@@ -180,6 +180,7 @@ function deriveLayer(
slots: tpl.slots,
caps: componentsToCaps(tpl.components),
metrics: tpl.metrics,
+ overview: tpl.overview,
};
}
const def = LAYER_DEFAULTS[rawKey] ?? DEFAULT_FOR_UNKNOWN_LAYER;
diff --git a/apps/ui/src/api/client.ts b/apps/ui/src/api/client.ts
index 932acca..03e78d6 100644
--- a/apps/ui/src/api/client.ts
+++ b/apps/ui/src/api/client.ts
@@ -72,8 +72,6 @@ export interface AdminLayerTemplate {
};
metrics: {
orderBy?: string;
- throughput?: string;
- spark?: string;
columns?: Array<{
metric: string;
label: string;
@@ -84,6 +82,10 @@ export interface AdminLayerTemplate {
precision?: number;
}>;
};
+ overview?: {
+ throughput?: string;
+ spark?: string;
+ };
widgets: DashboardWidget[];
}
diff --git a/apps/ui/src/composables/useLandingOrder.ts
b/apps/ui/src/composables/useLandingOrder.ts
index 3883b95..5a1fafe 100644
--- a/apps/ui/src/composables/useLandingOrder.ts
+++ b/apps/ui/src/composables/useLandingOrder.ts
@@ -32,8 +32,8 @@ export function useLandingOrder(layers: ComputedRef<readonly
LayerDef[]>) {
const store = useSetupStore();
return computed<LayerDef[]>(() => {
return [...layers.value].sort((a, b) => {
- const pa = store.ensure(a.key, { slots: a.slots, caps: a.caps, metrics:
a.metrics }).landing.priority;
- const pb = store.ensure(b.key, { slots: b.slots, caps: b.caps, metrics:
b.metrics }).landing.priority;
+ const pa = store.ensure(a.key, { slots: a.slots, caps: a.caps, metrics:
a.metrics, overview: a.overview }).landing.priority;
+ const pb = store.ensure(b.key, { slots: b.slots, caps: b.caps, metrics:
b.metrics, overview: b.overview }).landing.priority;
if (pa !== pb) return pa - pb;
return 0; // preserve incoming catalog order
});
diff --git a/apps/ui/src/stores/setup.ts b/apps/ui/src/stores/setup.ts
index 7da8fec..b204ccc 100644
--- a/apps/ui/src/stores/setup.ts
+++ b/apps/ui/src/stores/setup.ts
@@ -23,6 +23,7 @@ import type {
LayerCaps,
LayerConfig,
LayerMetricsConfig,
+ LayerOverviewConfig,
LayerSlots,
} from '@skywalking-horizon-ui/api-client';
import { bffClient } from '@/api/client';
@@ -78,7 +79,11 @@ function defaultAggregationFor(metricKey: string):
AggregationKind {
* Falls back to the static metric-catalog defaults when no template
* metrics arrived (e.g. layers without a JSON config file).
*/
-export function defaultLandingFor(layerKey: string, fromTemplate?:
LayerMetricsConfig): LandingConfig {
+export function defaultLandingFor(
+ layerKey: string,
+ fromTemplate?: LayerMetricsConfig,
+ fromOverview?: LayerOverviewConfig,
+): LandingConfig {
if (fromTemplate?.columns && fromTemplate.columns.length > 0) {
const cols = fromTemplate.columns.map((c) => ({
metric: c.metric,
@@ -90,8 +95,8 @@ export function defaultLandingFor(layerKey: string,
fromTemplate?: LayerMetricsC
...(c.precision !== undefined ? { precision: c.precision } : {}),
}));
const orderBy = fromTemplate.orderBy ?? cols[0].metric;
- const throughputMetric = fromTemplate.throughput ?? orderBy;
- const sparkMetric = fromTemplate.spark ?? throughputMetric;
+ const throughputMetric = fromOverview?.throughput ?? orderBy;
+ const sparkMetric = fromOverview?.spark ?? throughputMetric;
return {
priority: defaultPriority(layerKey),
topN: 5,
@@ -127,12 +132,17 @@ export function defaultLandingFor(layerKey: string,
fromTemplate?: LayerMetricsC
export function defaultLayerConfig(
layerKey: string,
- defaults: { slots: LayerSlots; caps: LayerCaps; metrics?: LayerMetricsConfig
},
+ defaults: {
+ slots: LayerSlots;
+ caps: LayerCaps;
+ metrics?: LayerMetricsConfig;
+ overview?: LayerOverviewConfig;
+ },
): LayerConfig {
return {
slots: { ...defaults.slots },
caps: { ...defaults.caps },
- landing: defaultLandingFor(layerKey, defaults.metrics),
+ landing: defaultLandingFor(layerKey, defaults.metrics, defaults.overview),
};
}
@@ -196,7 +206,12 @@ export const useSetupStore = defineStore('setup', () => {
*/
function ensure(
layerKey: string,
- defaults: { slots: LayerSlots; caps: LayerCaps; metrics?:
LayerMetricsConfig },
+ defaults: {
+ slots: LayerSlots;
+ caps: LayerCaps;
+ metrics?: LayerMetricsConfig;
+ overview?: LayerOverviewConfig;
+ },
): LayerConfig {
let cfg = configs[layerKey];
if (!cfg) {
@@ -208,7 +223,12 @@ export const useSetupStore = defineStore('setup', () => {
function reset(
layerKey: string,
- defaults: { slots: LayerSlots; caps: LayerCaps; metrics?:
LayerMetricsConfig },
+ defaults: {
+ slots: LayerSlots;
+ caps: LayerCaps;
+ metrics?: LayerMetricsConfig;
+ overview?: LayerOverviewConfig;
+ },
): void {
configs[layerKey] = defaultLayerConfig(layerKey, defaults);
markDirty();
diff --git a/apps/ui/src/views/admin/LayerDashboardsAdmin.vue
b/apps/ui/src/views/admin/LayerDashboardsAdmin.vue
index f789761..34f3860 100644
--- a/apps/ui/src/views/admin/LayerDashboardsAdmin.vue
+++ b/apps/ui/src/views/admin/LayerDashboardsAdmin.vue
@@ -169,11 +169,9 @@ const selectedTpl = computed(() => draft.template);
const currentWidgets = computed(() => widgetsFor(activeScope.value));
/**
- * Metrics-block editor. The metrics block lives directly on the
- * draft template; mutations flow through Vue's reactive proxy so the
- * dirty diff picks them up and Save enables. We ensure the
- * `metrics.columns` array exists before binding so the template can
- * safely v-model into it.
+ * Metrics block editor — drives the service-list columns + default
+ * sort. Overview-only fields (throughput, spark) live in a separate
+ * block, so they're edited in their own card.
*/
function ensureMetrics(): NonNullable<AdminLayerTemplate['metrics']> {
if (!draft.template) throw new Error('no template selected');
@@ -182,12 +180,23 @@ function ensureMetrics():
NonNullable<AdminLayerTemplate['metrics']> {
}
return draft.template.metrics as NonNullable<AdminLayerTemplate['metrics']>;
}
+function ensureOverview(): NonNullable<AdminLayerTemplate['overview']> {
+ if (!draft.template) throw new Error('no template selected');
+ if (!draft.template.overview) {
+ (draft.template as AdminLayerTemplate).overview = {};
+ }
+ return draft.template.overview as
NonNullable<AdminLayerTemplate['overview']>;
+}
const metricsModel = computed(() => {
if (!draft.template) return null;
- // Touch ensureMetrics on read so the keys are present for v-model.
ensureMetrics();
return draft.template.metrics as NonNullable<AdminLayerTemplate['metrics']>;
});
+const overviewModel = computed(() => {
+ if (!draft.template) return null;
+ ensureOverview();
+ return draft.template.overview as
NonNullable<AdminLayerTemplate['overview']>;
+});
const metricsColumns = computed(() => {
if (!draft.template) return [];
const m = ensureMetrics();
@@ -335,37 +344,23 @@ function toggleComponent(key: ComponentKey): void {
</div>
</section>
- <!-- Metrics editor (the layer's summary KPI columns + the
- orderBy / throughput / spark selectors). These drive the
- Overview KPI tile and the per-layer header summary. -->
+ <!-- Service-list metrics: the columns shown in the picker
+ zone's services table + the default sort. Used across
+ the per-layer page. -->
<section class="sw-card metrics-card">
<div class="card-head">
- <h4>Summary metrics</h4>
- <span class="sub">columns shown on the Overview KPI tile +
per-layer header</span>
+ <h4>Service list metrics</h4>
+ <span class="sub">columns + default sort for the service list
(picker zone)</span>
<button class="sw-btn add" type="button"
@click="addMetricColumn">+ Add column</button>
</div>
<div v-if="metricsModel" class="metrics-keys">
<label>
- <span>orderBy</span>
+ <span>Default sort (orderBy)</span>
<select v-model="metricsModel.orderBy">
<option :value="undefined">(first column)</option>
<option v-for="c in metricsColumns" :key="c.metric"
:value="c.metric">{{ c.metric }}</option>
</select>
</label>
- <label>
- <span>throughput</span>
- <select v-model="metricsModel.throughput">
- <option :value="undefined">(orderBy)</option>
- <option v-for="c in metricsColumns" :key="c.metric"
:value="c.metric">{{ c.metric }}</option>
- </select>
- </label>
- <label>
- <span>spark</span>
- <select v-model="metricsModel.spark">
- <option :value="undefined">(throughput)</option>
- <option v-for="c in metricsColumns" :key="c.metric"
:value="c.metric">{{ c.metric }}</option>
- </select>
- </label>
</div>
<div v-if="metricsColumns.length === 0" class="empty inset">
No metric columns defined. Click "Add column" to start.
@@ -405,6 +400,32 @@ function toggleComponent(key: ComponentKey): void {
</table>
</section>
+ <!-- Overview-tile settings: the per-layer compact tile on the
+ Overview's top strip. Only these two settings live here;
+ they reference metric keys from the service-list columns. -->
+ <section v-if="overviewModel" class="sw-card overview-card">
+ <div class="card-head">
+ <h4>Overview tile</h4>
+ <span class="sub">per-layer compact tile on the Overview's top
strip</span>
+ </div>
+ <div class="metrics-keys">
+ <label>
+ <span>Headline (throughput)</span>
+ <select v-model="overviewModel.throughput">
+ <option :value="undefined">(orderBy)</option>
+ <option v-for="c in metricsColumns" :key="c.metric"
:value="c.metric">{{ c.metric }}</option>
+ </select>
+ </label>
+ <label>
+ <span>Trend line (spark)</span>
+ <select v-model="overviewModel.spark">
+ <option :value="undefined">(throughput)</option>
+ <option v-for="c in metricsColumns" :key="c.metric"
:value="c.metric">{{ c.metric }}</option>
+ </select>
+ </label>
+ </div>
+ </section>
+
<!-- Scope tabs -->
<nav class="scope-tabs sw-card">
<button
@@ -773,7 +794,25 @@ function toggleComponent(key: ComponentKey): void {
font-weight: 500;
}
-.metrics-card { padding: 0; }
+.metrics-card,
+.overview-card { padding: 0; }
+.overview-card .card-head {
+ display: flex;
+ align-items: baseline;
+ gap: 10px;
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--sw-line);
+}
+.overview-card .card-head h4 {
+ margin: 0;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--sw-fg-0);
+}
+.overview-card .card-head .sub {
+ font-size: 10.5px;
+ color: var(--sw-fg-3);
+}
.metrics-card .card-head .add {
margin-left: auto;
font-size: 11.5px;
diff --git a/apps/ui/src/views/layer/LayerDashboardsView.vue
b/apps/ui/src/views/layer/LayerDashboardsView.vue
index b3ef1fc..deb2ec5 100644
--- a/apps/ui/src/views/layer/LayerDashboardsView.vue
+++ b/apps/ui/src/views/layer/LayerDashboardsView.vue
@@ -63,7 +63,7 @@ const safeLayer = computed<LayerDef>(() => layer.value ?? {
});
const safeCfg = computed(() => {
if (!layer.value) return { priority: 99, topN: 5, orderBy: 'cpm', columns:
[], style: 'table' as const };
- return store.ensure(layer.value.key, { slots: layer.value.slots, caps:
layer.value.caps, metrics: layer.value.metrics }).landing;
+ return store.ensure(layer.value.key, { slots: layer.value.slots, caps:
layer.value.caps, metrics: layer.value.metrics, overview: layer.value.overview
}).landing;
});
const landing = useLayerLanding(safeLayer, safeCfg);
const serviceName = computed<string | null>(() => {
diff --git a/apps/ui/src/views/layer/LayerShell.vue
b/apps/ui/src/views/layer/LayerShell.vue
index 1cbb2cf..f5f8fe4 100644
--- a/apps/ui/src/views/layer/LayerShell.vue
+++ b/apps/ui/src/views/layer/LayerShell.vue
@@ -50,7 +50,7 @@ const layer = computed<LayerDef | null>(() => {
const store = useSetupStore();
const cfg = computed(() => {
if (!layer.value) return null;
- return store.ensure(layer.value.key, { slots: layer.value.slots, caps:
layer.value.caps, metrics: layer.value.metrics });
+ return store.ensure(layer.value.key, { slots: layer.value.slots, caps:
layer.value.caps, metrics: layer.value.metrics, overview: layer.value.overview
});
});
// Build a non-null LayerDef ref for the landing composable.
diff --git a/apps/ui/src/views/overview/LayerKpiStripCard.vue
b/apps/ui/src/views/overview/LayerKpiStripCard.vue
index 32c15ea..77f0373 100644
--- a/apps/ui/src/views/overview/LayerKpiStripCard.vue
+++ b/apps/ui/src/views/overview/LayerKpiStripCard.vue
@@ -36,7 +36,7 @@ import Sparkline from '@/components/charts/Sparkline.vue';
const props = defineProps<{ layer: LayerDef }>();
const store = useSetupStore();
const cfg = computed(() =>
- store.ensure(props.layer.key, { slots: props.layer.slots, caps:
props.layer.caps, metrics: props.layer.metrics }),
+ store.ensure(props.layer.key, { slots: props.layer.slots, caps:
props.layer.caps, metrics: props.layer.metrics, overview: props.layer.overview
}),
);
const landingCfg = computed(() => cfg.value.landing);
const layerRef = toRef(props, 'layer');
diff --git a/apps/ui/src/views/overview/LayerKpiTile.vue
b/apps/ui/src/views/overview/LayerKpiTile.vue
index 62a82f9..0847210 100644
--- a/apps/ui/src/views/overview/LayerKpiTile.vue
+++ b/apps/ui/src/views/overview/LayerKpiTile.vue
@@ -37,7 +37,7 @@ import Sparkline from '@/components/charts/Sparkline.vue';
const props = defineProps<{ layer: LayerDef }>();
const store = useSetupStore();
const cfg = computed(() =>
- store.ensure(props.layer.key, { slots: props.layer.slots, caps:
props.layer.caps, metrics: props.layer.metrics }),
+ store.ensure(props.layer.key, { slots: props.layer.slots, caps:
props.layer.caps, metrics: props.layer.metrics, overview: props.layer.overview
}),
);
const landingCfg = computed(() => cfg.value.landing);
const layerRef = toRef(props, 'layer');
diff --git a/apps/ui/src/views/overview/LayerLandingCard.vue
b/apps/ui/src/views/overview/LayerLandingCard.vue
index 795938f..c69eaca 100644
--- a/apps/ui/src/views/overview/LayerLandingCard.vue
+++ b/apps/ui/src/views/overview/LayerLandingCard.vue
@@ -27,7 +27,7 @@ import { fmtMetric } from '@/utils/formatters';
const props = defineProps<{ layer: LayerDef }>();
const store = useSetupStore();
-const cfg = computed(() => store.ensure(props.layer.key, { slots:
props.layer.slots, caps: props.layer.caps, metrics: props.layer.metrics }));
+const cfg = computed(() => store.ensure(props.layer.key, { slots:
props.layer.slots, caps: props.layer.caps, metrics: props.layer.metrics,
overview: props.layer.overview }));
const slotName = computed(() => cfg.value.slots.services ?? 'Services');
const detailHref = computed(() => `/layer/${props.layer.key}/services`);
diff --git a/apps/ui/src/views/setup/LayerSetupCard.vue
b/apps/ui/src/views/setup/LayerSetupCard.vue
index ec3c67d..5869564 100644
--- a/apps/ui/src/views/setup/LayerSetupCard.vue
+++ b/apps/ui/src/views/setup/LayerSetupCard.vue
@@ -47,7 +47,7 @@ const props = defineProps<{ layer: LayerDef; expanded?:
boolean }>();
const emit = defineEmits<{ (e: 'toggle'): void }>();
const store = useSetupStore();
-const cfg = computed(() => store.ensure(props.layer.key, { slots:
props.layer.slots, caps: props.layer.caps, metrics: props.layer.metrics }));
+const cfg = computed(() => store.ensure(props.layer.key, { slots:
props.layer.slots, caps: props.layer.caps, metrics: props.layer.metrics,
overview: props.layer.overview }));
const open = ref(props.expanded ?? false);
function toggle(): void {
diff --git a/apps/ui/src/views/setup/SetupView.vue
b/apps/ui/src/views/setup/SetupView.vue
index 584496e..2cd55a8 100644
--- a/apps/ui/src/views/setup/SetupView.vue
+++ b/apps/ui/src/views/setup/SetupView.vue
@@ -124,7 +124,7 @@ const visibleLayers = computed(() => {
<strong>{{ orderedLayers.length }}</strong> layer(s) in this
deployment,
rendered on the Overview in priority order:
<span v-for="(L, i) in orderedLayers.slice(0, 8)" :key="L.key"
class="chip-name">
- {{ L.name }} ({{ store.ensure(L.key, { slots: L.slots, caps:
L.caps, metrics: L.metrics }).landing.priority }})<span
+ {{ L.name }} ({{ store.ensure(L.key, { slots: L.slots, caps:
L.caps, metrics: L.metrics, overview: L.overview }).landing.priority }})<span
v-if="i < Math.min(orderedLayers.length, 8) - 1"
>,</span>
</span>
diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts
index 9d4309b..99a65ec 100644
--- a/packages/api-client/src/index.ts
+++ b/packages/api-client/src/index.ts
@@ -22,6 +22,7 @@ export type {
LayerDef,
LayerMetricsColumn,
LayerMetricsConfig,
+ LayerOverviewConfig,
MenuResponse,
} from './menu.js';
export type {
diff --git a/packages/api-client/src/menu.ts b/packages/api-client/src/menu.ts
index 5d6937c..988df89 100644
--- a/packages/api-client/src/menu.ts
+++ b/packages/api-client/src/menu.ts
@@ -66,14 +66,31 @@ export interface LayerMetricsColumn {
precision?: number;
}
+/**
+ * Metrics block on a layer template — defines the columns used
+ * across the service list, plus the default sort. Overview-tile
+ * settings (headline metric, trend metric) live separately on
+ * `LayerOverviewConfig` since they're only used by the Overview
+ * page; this block drives the service list table on the per-layer
+ * page.
+ */
export interface LayerMetricsConfig {
- /** Default `topN` ranking metric. */
+ /** Default sort metric for the service list. */
orderBy?: string;
- /** Headline metric for the Overview per-layer KPI tile. */
+ columns?: LayerMetricsColumn[];
+}
+
+/**
+ * Overview-page-only settings. The per-layer compact tile on the
+ * Overview's top strip picks its big headline value and its
+ * trend-line metric from here. Both reference metric keys present
+ * in `LayerMetricsConfig.columns`.
+ */
+export interface LayerOverviewConfig {
+ /** Metric key for the Overview tile's big headline value. */
throughput?: string;
- /** Sparkline metric (defaults to `throughput` when omitted). */
+ /** Metric key for the Overview tile's trend line. */
spark?: string;
- columns?: LayerMetricsColumn[];
}
export interface LayerDef {
@@ -93,9 +110,13 @@ export interface LayerDef {
slots: LayerSlots;
caps: LayerCaps;
/** Per-layer metric config from the JSON template; UI uses it as
- * the source of truth for KPI columns + throughput / spark when
- * present. Falls back to static catalog defaults when absent. */
+ * the source of truth for the service-list columns + default
+ * sort. Falls back to static catalog defaults when absent. */
metrics?: LayerMetricsConfig;
+ /** Overview-tile settings — the headline metric + trend metric on
+ * the per-layer compact tile in the Overview's top strip. Empty
+ * when the layer template omits the `overview` block. */
+ overview?: LayerOverviewConfig;
}
export interface MenuResponse {