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 d5e4945  topList: ?mockTop=N pad, tighter rows; line legend: top 
spacing fix
d5e4945 is described below

commit d5e4945e7c778102fdd1f3b535740ecd512d9e35
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 12 22:03:01 2026 +0800

    topList: ?mockTop=N pad, tighter rows; line legend: top spacing fix
    
    Dev-mode pad for sizing tests: append ?mockTop=10 to any per-layer
    URL (e.g. /layer/general/service?mockTop=10) and the BFF pads every
    TopList result to 10 synthetic rows. Names tail from the last real
    row so the list reads as ranked, values taper. Cap at 40, off when
    the param is absent / 0.
    
    TopList row sizing tightened so a span-3 rowSpan-3 widget (~360px
    total height, ~300px usable) shows 10 rows without overflow scroll
    in the typical case:
      gap 4 → 3 px
      font 11 → 10.5 px
      padding 1 → 0
      value column 64 → 56 px
      line-height 1.3 explicit
    ~22 px per row → 10 rows ≈ 220 px, fits comfortably.
    
    TimeChart top-legend spacing: the legend was hugging the chart edge.
    Bumps grid.top from 22 → 28 when legend is visible and gives the
    legend padding=0 + top=2 + lineHeight=12 so the chips sit
    consistently 6 px below the card head and 12 px above the plot
    area. Single-series widgets get grid.top=10 for a small breath.
---
 apps/bff/src/dashboard/routes.ts                | 40 ++++++++++++++++++++-----
 apps/ui/src/api/client.ts                       |  7 ++++-
 apps/ui/src/components/charts/TimeChart.vue     | 13 ++++----
 apps/ui/src/components/charts/TopList.vue       |  9 +++---
 apps/ui/src/composables/useLayerDashboard.ts    | 23 ++++++++++----
 apps/ui/src/views/layer/LayerDashboardsView.vue | 12 +++++++-
 6 files changed, 80 insertions(+), 24 deletions(-)

diff --git a/apps/bff/src/dashboard/routes.ts b/apps/bff/src/dashboard/routes.ts
index 76965f7..9e005a7 100644
--- a/apps/bff/src/dashboard/routes.ts
+++ b/apps/bff/src/dashboard/routes.ts
@@ -276,6 +276,11 @@ export function registerDashboardRoute(app: 
FastifyInstance, deps: DashboardRout
       if (!parsed.success) {
         return reply.code(400).send({ error: 'invalid_body', detail: 
parsed.error.flatten() });
       }
+      // Dev-mode query param `?mockTop=N` pads every TopList result to
+      // exactly N entries with synthetic rows. Use to verify widget
+      // height / overflow without waiting for OAP to populate the layer.
+      const mockTopRaw = (req.query as { mockTop?: string }).mockTop;
+      const mockTopN = mockTopRaw ? Math.max(0, Math.min(40, 
Number(mockTopRaw))) : 0;
       const scope = parsed.data.scope ?? 'service';
       const tpl = getLayerTemplate(layerKey);
       const widgets: DashboardWidget[] =
@@ -368,11 +373,6 @@ export function registerDashboardRoute(app: 
FastifyInstance, deps: DashboardRout
       //  - 'top':  extract sorted list from the first expression
       const results: DashboardWidgetResult[] = widgets.map((widget, wIdx) => {
         if (widget.type === 'top') {
-          // Every expression contributes one switchable group (e.g.
-          // "Top by traffic" / "Top slowest" / "Top by SR"). The UI
-          // renders a tab per group and shows the active one; the
-          // expression travels along so the tab tooltip can surface
-          // the MQE.
           const groups: Array<{
             label: string;
             expression: string;
@@ -380,7 +380,33 @@ export function registerDashboardRoute(app: 
FastifyInstance, deps: DashboardRout
             items: NonNullable<ReturnType<typeof parseTopList>>;
           }> = [];
           widget.expressions.forEach((expr, eIdx) => {
-            const items = parseTopList(data[`w${wIdx}_e${eIdx}`]);
+            let items = parseTopList(data[`w${wIdx}_e${eIdx}`]);
+            // Pad with synthetic rows when mockTop is requested. Each
+            // padded row gets a plausible name + a value tapered down
+            // from the last real row so the list reads as ranked.
+            if (mockTopN > 0) {
+              const current = items ?? [];
+              const padCount = Math.max(0, mockTopN - current.length);
+              if (padCount > 0) {
+                const last = current[current.length - 1]?.value ?? 100;
+                const seed = current[current.length - 1]?.name ?? 
'mock-service';
+                const padded = Array.from({ length: padCount }, (_, i) => {
+                  const idx = current.length + i + 1;
+                  const decay = 1 - (i + 1) / (padCount + 2);
+                  return {
+                    name: `${seed} · mock-${idx}`,
+                    value: typeof last === 'number' ? Math.round(last * decay 
* 100) / 100 : 0,
+                  };
+                });
+                items = [...current, ...padded];
+              }
+              if (!items || items.length === 0) {
+                items = Array.from({ length: mockTopN }, (_, i) => ({
+                  name: `mock-entity-${i + 1}`,
+                  value: Math.round((100 - i * 8) * 100) / 100,
+                }));
+              }
+            }
             if (!items) return;
             const label = widget.expressionLabels?.[eIdx] ?? expr;
             const unit = widget.expressionUnits?.[eIdx];
@@ -395,8 +421,6 @@ export function registerDashboardRoute(app: 
FastifyInstance, deps: DashboardRout
           return {
             id: widget.id,
             topGroups: groups,
-            // Keep `topList` set to the first group's items for any
-            // older SPA paths that still read the flat field.
             topList: groups[0].items,
           };
         }
diff --git a/apps/ui/src/api/client.ts b/apps/ui/src/api/client.ts
index 6247ba2..a13827b 100644
--- a/apps/ui/src/api/client.ts
+++ b/apps/ui/src/api/client.ts
@@ -197,10 +197,15 @@ export class BffClient {
   dashboard(
     layerKey: string,
     body: { service?: string; widgets?: DashboardWidget[]; scope?: string } = 
{},
+    /** Dev-mode mock: pad every TopList result to N entries with
+     *  synthetic rows so operators can verify widget sizing without
+     *  waiting for live data. Forwarded as `?mockTop=N`. */
+    opts: { mockTop?: number } = {},
   ): Promise<DashboardResponse> {
+    const qs = opts.mockTop && opts.mockTop > 0 ? `?mockTop=${opts.mockTop}` : 
'';
     return this.request<DashboardResponse>(
       'POST',
-      `/api/layer/${encodeURIComponent(layerKey)}/dashboard`,
+      `/api/layer/${encodeURIComponent(layerKey)}/dashboard${qs}`,
       body,
     );
   }
diff --git a/apps/ui/src/components/charts/TimeChart.vue 
b/apps/ui/src/components/charts/TimeChart.vue
index 2b9cc4f..233cdeb 100644
--- a/apps/ui/src/components/charts/TimeChart.vue
+++ b/apps/ui/src/components/charts/TimeChart.vue
@@ -110,18 +110,21 @@ function buildOption(): echarts.EChartsCoreOption {
     },
     legend: {
       show: props.series.length > 1,
-      top: 0,
-      left: 0,
-      textStyle: { color: '#94a3b8', fontSize: 10 },
+      top: 2,
+      left: 4,
+      padding: [0, 0, 0, 0],
+      textStyle: { color: '#94a3b8', fontSize: 10, lineHeight: 12 },
       itemWidth: 10,
       itemHeight: 8,
-      itemGap: 12,
+      itemGap: 10,
       icon: 'roundRect',
     },
     grid: {
       left: 36,
       right: props.series.some((s) => (s.yAxisIndex ?? 0) === 1) ? 32 : 8,
-      top: props.series.length > 1 ? 22 : 8,
+      // When legend renders we need ~18px above the plot for the chips
+      // and a few px breathing room before the top axis label kicks in.
+      top: props.series.length > 1 ? 28 : 10,
       bottom: 8,
       containLabel: false,
     },
diff --git a/apps/ui/src/components/charts/TopList.vue 
b/apps/ui/src/components/charts/TopList.vue
index 194f031..714aca8 100644
--- a/apps/ui/src/components/charts/TopList.vue
+++ b/apps/ui/src/components/charts/TopList.vue
@@ -154,7 +154,7 @@ const showTabs = computed(() => 
effectiveGroups.value.length > 1);
 .rows {
   display: flex;
   flex-direction: column;
-  gap: 4px;
+  gap: 3px;
   padding: 2px 2px 4px;
   overflow-y: auto;
   flex: 1;
@@ -162,11 +162,12 @@ const showTabs = computed(() => 
effectiveGroups.value.length > 1);
 }
 .row {
   display: grid;
-  grid-template-columns: 18px 1fr 48px 64px;
+  grid-template-columns: 18px 1fr 48px 56px;
   align-items: center;
   gap: 6px;
-  font-size: 11px;
-  padding: 1px 0;
+  font-size: 10.5px;
+  padding: 0;
+  line-height: 1.3;
 }
 .rank {
   font-family: var(--sw-mono);
diff --git a/apps/ui/src/composables/useLayerDashboard.ts 
b/apps/ui/src/composables/useLayerDashboard.ts
index 703d52c..9825605 100644
--- a/apps/ui/src/composables/useLayerDashboard.ts
+++ b/apps/ui/src/composables/useLayerDashboard.ts
@@ -49,14 +49,27 @@ export function useLayerDashboard(
   layerKey: Ref<string>,
   service: Ref<string | null>,
   scope?: Ref<string>,
+  /** Optional `?mockTop=N` passthrough — when set, every TopList in
+   *  the response is padded to N synthetic rows for UI sizing tests. */
+  mockTop?: Ref<number>,
 ) {
   const q = useQuery({
-    queryKey: ['dashboard', layerKey, service, scope ?? computed(() => 
'service')],
+    queryKey: [
+      'dashboard',
+      layerKey,
+      service,
+      scope ?? computed(() => 'service'),
+      mockTop ?? computed(() => 0),
+    ],
     queryFn: () =>
-      bffClient.dashboard(layerKey.value, {
-        ...(service.value ? { service: service.value } : {}),
-        ...(scope?.value ? { scope: scope.value } : {}),
-      }),
+      bffClient.dashboard(
+        layerKey.value,
+        {
+          ...(service.value ? { service: service.value } : {}),
+          ...(scope?.value ? { scope: scope.value } : {}),
+        },
+        mockTop?.value ? { mockTop: mockTop.value } : {},
+      ),
     enabled: computed(() => layerKey.value.length > 0),
     staleTime: 45_000,
     refetchInterval: 60_000,
diff --git a/apps/ui/src/views/layer/LayerDashboardsView.vue 
b/apps/ui/src/views/layer/LayerDashboardsView.vue
index 171c83b..7ca6319 100644
--- a/apps/ui/src/views/layer/LayerDashboardsView.vue
+++ b/apps/ui/src/views/layer/LayerDashboardsView.vue
@@ -72,8 +72,18 @@ const serviceName = computed<string | null>(() => {
   return match?.serviceName ?? null;
 });
 
+// Dev-only escape hatch: appending `?mockTop=10` to the page URL pads
+// every TopList result to N synthetic rows. Helps operators verify
+// widget heights without waiting for OAP to populate the layer.
+const mockTop = computed<number>(() => {
+  const v = route.query.mockTop;
+  if (typeof v !== 'string') return 0;
+  const n = Number(v);
+  return Number.isFinite(n) && n > 0 ? Math.min(40, n) : 0;
+});
+
 const { config, isLoading: configLoading } = useLayerDashboardConfig(layerKey, 
scope);
-const { data, isFetching, error } = useLayerDashboard(layerKey, serviceName, 
scope);
+const { data, isFetching, error } = useLayerDashboard(layerKey, serviceName, 
scope, mockTop);
 
 const widgets = computed(() => config.value?.widgets ?? []);
 const resultsById = computed(() => {

Reply via email to