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. */
