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

commit fc6c34b6c13c09040d66f3fe1c51953f2226413d
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 12 21:35:20 2026 +0800

    widgets: layerScope flag, drop redundant p99, legend on top, error-rate line
    
    Top 20 endpoints widget gains layerScope:true — BFF now passes
    `entity: { scope: All }` for that widget so the top_n runs across
    the whole layer rather than the currently selected service. Matches
    operator intent: 'should be per layer, not under current service.'
    
    New optional DashboardWidget.layerScope flag (boolean). When set the
    MQE entity flips to { scope: All }, no serviceName filter. Other
    widgets keep service-scoped queries.
    
    Service dashboard row 2 cleanup:
      - Drop standalone p99 widget — redundant given the percentile chart
        already plots p50/75/90/95/99 as a labeled multi-series.
      - Add Error Rate line (100 - service_sla/100) to fill the third
        slot, color-tinted err-red so it matches the err KPI in the layer
        header summary.
    
    Multi-series line widgets (percentile + future relabels charts):
    legend now renders at the TOP of the chart instead of the bottom —
    the relabels-derived labels (p50/75/...) read more naturally as a
    caption than a footer.
---
 apps/bff/src/dashboard/routes.ts            | 17 +++++++++++++++--
 apps/bff/src/layers/config/general.json     | 21 +++++++++++----------
 apps/ui/src/components/charts/TimeChart.vue |  8 +++++---
 packages/api-client/src/dashboard.ts        |  7 +++++++
 4 files changed, 38 insertions(+), 15 deletions(-)

diff --git a/apps/bff/src/dashboard/routes.ts b/apps/bff/src/dashboard/routes.ts
index 4b2649e..f9ce08e 100644
--- a/apps/bff/src/dashboard/routes.ts
+++ b/apps/bff/src/dashboard/routes.ts
@@ -68,6 +68,7 @@ const widgetSchema = z.object({
   span: z.number().int().min(1).max(12).optional(),
   rowSpan: z.number().int().min(1).max(64).optional(),
   visibleWhen: z.string().optional(),
+  layerScope: z.boolean().optional(),
   // Legacy x/y/w/h kept optional for back-compat.
   x: z.number().int().min(0).optional(),
   y: z.number().int().min(0).optional(),
@@ -142,15 +143,23 @@ function buildFragment(
   serviceName: string,
   normal: boolean,
   w: Window,
+  opts: { layerScope?: boolean } = {},
 ): string {
   // We fetch metric.labels (for multi-series Line widgets — relabels()
   // returns one labeled result per percentile) and value.id /
   // owner.endpointName (for TopList widgets — top_n() returns a
   // sorted list of entities + values).
+  //
+  // layerScope=true skips the serviceName filter so the MQE runs
+  // across the whole layer — used for cross-service rollups like the
+  // "Top 20 endpoints" widget on the per-layer Service page.
+  const entity = opts.layerScope
+    ? '{ scope: All }'
+    : `{ scope: Service, serviceName: ${JSON.stringify(serviceName)}, normal: 
${normal ? 'true' : 'false'} }`;
   return (
     `${alias}: execExpression(\n` +
     `      expression: ${JSON.stringify(expression)},\n` +
-    `      entity: { scope: Service, serviceName: 
${JSON.stringify(serviceName)}, normal: ${normal ? 'true' : 'false'} },\n` +
+    `      entity: ${entity},\n` +
     `      duration: { start: ${JSON.stringify(w.start)}, end: 
${JSON.stringify(w.end)}, step: MINUTE }\n` +
     `    ) {\n` +
     `      type error\n` +
@@ -312,7 +321,11 @@ export function registerDashboardRoute(app: 
FastifyInstance, deps: DashboardRout
         widget.expressions.forEach((expr, eIdx) => {
           const alias = `w${wIdx}_e${eIdx}`;
           aliasMap.set(alias, { wIdx, eIdx });
-          fragments.push(buildFragment(alias, expr, serviceName, normal, 
window));
+          fragments.push(
+            buildFragment(alias, expr, serviceName, normal, window, {
+              layerScope: widget.layerScope === true,
+            }),
+          );
         });
       });
       let data: Record<string, MqeResultShape> = {};
diff --git a/apps/bff/src/layers/config/general.json 
b/apps/bff/src/layers/config/general.json
index 25e1385..cec10ba 100644
--- a/apps/bff/src/layers/config/general.json
+++ b/apps/bff/src/layers/config/general.json
@@ -35,10 +35,11 @@
       {
         "id": "top_endpoints",
         "title": "Top 20 endpoints by traffic",
-        "tip": "top_n(endpoint_cpm,20,des) — click an endpoint to drill in.",
+        "tip": "top_n(endpoint_cpm,20,des) — scoped to the whole layer (not 
the selected service).",
         "type": "top",
         "unit": "rpm",
         "expressions": ["top_n(endpoint_cpm,20,des)"],
+        "layerScope": true,
         "span": 3,
         "rowSpan": 4
       },
@@ -78,15 +79,6 @@
         "span": 3,
         "rowSpan": 2
       },
-      {
-        "id": "p99_line",
-        "title": "p99",
-        "type": "line",
-        "unit": "ms",
-        "expressions": ["service_percentile{p='99'}"],
-        "span": 3,
-        "rowSpan": 2
-      },
       {
         "id": "percentile_line",
         "title": "Response Time Percentile",
@@ -98,6 +90,15 @@
         ],
         "span": 3,
         "rowSpan": 2
+      },
+      {
+        "id": "err_line",
+        "title": "Error Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["100 - service_sla/100"],
+        "span": 3,
+        "rowSpan": 2
       }
     ],
     "instance": [
diff --git a/apps/ui/src/components/charts/TimeChart.vue 
b/apps/ui/src/components/charts/TimeChart.vue
index 55d4546..118d09a 100644
--- a/apps/ui/src/components/charts/TimeChart.vue
+++ b/apps/ui/src/components/charts/TimeChart.vue
@@ -107,17 +107,19 @@ function buildOption(): echarts.EChartsCoreOption {
     },
     legend: {
       show: props.series.length > 1,
-      bottom: 0,
+      top: 0,
+      left: 0,
       textStyle: { color: '#94a3b8', fontSize: 10 },
       itemWidth: 10,
       itemHeight: 8,
+      itemGap: 12,
       icon: 'roundRect',
     },
     grid: {
       left: 36,
       right: 8,
-      top: 8,
-      bottom: props.series.length > 1 ? 24 : 8,
+      top: props.series.length > 1 ? 22 : 8,
+      bottom: 8,
       containLabel: false,
     },
     xAxis: {
diff --git a/packages/api-client/src/dashboard.ts 
b/packages/api-client/src/dashboard.ts
index 10db62a..ba7758f 100644
--- a/packages/api-client/src/dashboard.ts
+++ b/packages/api-client/src/dashboard.ts
@@ -72,6 +72,13 @@ export interface DashboardWidget {
    * Future-compatible; the SPA evaluates this client-side.
    */
   visibleWhen?: string;
+  /**
+   * When true, the BFF runs this widget's MQE against the whole layer
+   * rather than scoping it to the currently-selected service. Used for
+   * cross-service rollups (e.g. "Top 20 endpoints by traffic across the
+   * layer"). MQE entity flips to `{ scope: All }`.
+   */
+  layerScope?: boolean;
   /** Legacy 24-col grid coordinates — kept for back-compat during the
    *  span-based flow-layout migration. New widgets should leave these
    *  unset and use `span` / `rowSpan` instead. */

Reply via email to