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 b489ff0437dc2d6a6471278217163e56822e9586 Author: Wu Sheng <[email protected]> AuthorDate: Tue May 12 17:23:32 2026 +0800 layer: drop crufty header tags + chrome, fix service-name plumbing Layer header card: - Drop the LAYER tag and 'OAP layer · level N' tag — operators don't need either; the layer dot + name already identifies the layer. - Restore layer-wide aggregate KPIs in the summary row (sum/avg per the operator's setup config). The selected-service context lives on the Switch button label below; KPIs themselves are the layer rollup again, as before. Service page (LayerDashboardsView): - Header chrome trimmed to just the service name + a 'refreshing / OAP unreachable' state pill. Drops the '7 widgets', the base64 id in 'MQE scoped to ...', and the 'Refreshes every 60s. Customize widgets.' blurb operators don't act on. - Service-name bug fix: the URL ?service= carries the base64 OAP id, but MQE entities are keyed by serviceName. View now looks the row up in the layer landing data (shared vue-query cache) and passes the actual serviceName to /api/layer/:key/dashboard. Widgets that previously came back empty should now render. --- apps/ui/src/views/layer/LayerDashboardsView.vue | 80 ++++++++++++------------- apps/ui/src/views/layer/LayerShell.vue | 29 ++------- 2 files changed, 43 insertions(+), 66 deletions(-) diff --git a/apps/ui/src/views/layer/LayerDashboardsView.vue b/apps/ui/src/views/layer/LayerDashboardsView.vue index 066c872..6372c45 100644 --- a/apps/ui/src/views/layer/LayerDashboardsView.vue +++ b/apps/ui/src/views/layer/LayerDashboardsView.vue @@ -27,20 +27,44 @@ --> <script setup lang="ts"> import { computed } from 'vue'; -import { useRoute, RouterLink } from 'vue-router'; +import { useRoute } from 'vue-router'; +import type { LayerDef } from '@skywalking-horizon-ui/api-client'; import TimeChart from '@/components/charts/TimeChart.vue'; -import { - useLayerDashboard, - useLayerDashboardConfig, -} from '@/composables/useLayerDashboard'; +import { useLayerDashboard, useLayerDashboardConfig } from '@/composables/useLayerDashboard'; +import { useLayerLanding } from '@/composables/useLayerLanding'; +import { useLayers } from '@/composables/useLayers'; import { useSelectedService } from '@/composables/useSelectedService'; +import { useSetupStore } from '@/stores/setup'; import { fmtMetric } from '@/utils/formatters'; const route = useRoute(); const layerKey = computed(() => String(route.params.layerKey ?? '')); const { selectedId } = useSelectedService(); +const { layers } = useLayers(); +const layer = computed<LayerDef | null>(() => layers.value.find((l) => l.key === layerKey.value) ?? null); + +// Look up the service NAME from landing data — selectedId is the +// base64 OAP service id, which MQE doesn't accept; MQE entities are +// keyed by serviceName. We share the landing query with the rest of +// the per-layer page so this is free (cached by vue-query). +const store = useSetupStore(); +const safeLayer = computed<LayerDef>(() => layer.value ?? { + key: layerKey.value, name: layerKey.value, color: 'var(--sw-fg-2)', + serviceCount: -1, active: false, level: null, slots: {}, caps: {}, +}); +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 }).landing; +}); +const landing = useLayerLanding(safeLayer, safeCfg); +const serviceName = computed<string | null>(() => { + const rows = landing.data.value?.sampledRows ?? landing.rows.value ?? []; + const match = rows.find((r) => r.serviceId === selectedId.value); + return match?.serviceName ?? null; +}); + const { config, isLoading: configLoading } = useLayerDashboardConfig(layerKey); -const { data, isFetching, error } = useLayerDashboard(layerKey, selectedId); +const { data, isFetching, error } = useLayerDashboard(layerKey, serviceName); const widgets = computed(() => config.value?.widgets ?? []); const resultsById = computed(() => { @@ -48,26 +72,18 @@ const resultsById = computed(() => { for (const r of data.value?.widgets ?? []) out.set(r.id, r); return out; }); -const serviceText = computed(() => data.value?.service ?? selectedId.value ?? '(auto)'); const reachable = computed(() => data.value?.reachable !== false); const errorText = computed(() => data.value?.error ?? (error.value ? String(error.value) : null)); +const headerTitle = computed(() => serviceName.value ?? data.value?.service ?? 'Pick a service'); </script> <template> <div class="dash-tab"> <header class="dash-head"> - <div> - <div class="kicker">Service</div> - <h2>{{ widgets.length }} widget{{ widgets.length === 1 ? '' : 's' }}</h2> - <p class="sub"> - MQE scoped to <code>{{ serviceText }}</code>. - Refreshes every 60s. <RouterLink to="/admin/layer-dashboards">Customize widgets</RouterLink>. - </p> - </div> + <h2 class="svc-title">{{ headerTitle }}</h2> <div class="state"> <span v-if="isFetching" class="badge fetch">refreshing</span> <span v-else-if="!reachable" class="badge err">OAP unreachable</span> - <span v-else class="badge ok">live</span> </div> </header> @@ -130,38 +146,16 @@ const errorText = computed(() => data.value?.error ?? (error.value ? String(erro } .dash-head { display: flex; - align-items: flex-start; + align-items: center; gap: 12px; } -.kicker { - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--sw-accent); -} -.dash-head h2 { - margin: 2px 0 4px; - font-size: 15px; +.svc-title { + margin: 0; + font-size: 14px; font-weight: 600; color: var(--sw-fg-0); - letter-spacing: -0.01em; -} -.dash-head .sub { - margin: 0; - font-size: 11px; - color: var(--sw-fg-2); - line-height: 1.5; -} -.dash-head .sub code { font-family: var(--sw-mono); - font-size: 10.5px; - background: var(--sw-bg-2); - padding: 1px 4px; - border-radius: 3px; -} -.dash-head .sub a { - color: var(--sw-accent-2); - text-decoration: none; + letter-spacing: -0.01em; } .state { margin-left: auto; diff --git a/apps/ui/src/views/layer/LayerShell.vue b/apps/ui/src/views/layer/LayerShell.vue index bdd7851..0c6cda0 100644 --- a/apps/ui/src/views/layer/LayerShell.vue +++ b/apps/ui/src/views/layer/LayerShell.vue @@ -129,12 +129,10 @@ interface HeaderKpi { spark?: Array<number | null>; } /** - * Header KPIs scope to the *selected service*. Falls back to the - * layer-wide aggregates when no service is selectable (e.g. before - * data loads). The per-service sparkline comes from `row.spark` when - * present; otherwise falls through to the layer-wide - * `seriesByMetric[col.metric]` so the trend area never goes blank just - * because the BFF only built one spark series. + * Header KPIs are layer-wide aggregates — sum or avg across the topN + * services per the operator's setup config. The Switch button below + * carries the *selected service* context for the widget grid; the + * header summary is the layer rollup. */ const headerKpis = computed<HeaderKpi[]>(() => { const L = layer.value; @@ -142,33 +140,20 @@ const headerKpis = computed<HeaderKpi[]>(() => { const c = cfg.value; if (!c) return []; const a = aggregates.value; - const row = selectedRow.value; const out: HeaderKpi[] = []; for (const col of c.landing.columns.slice(0, 5)) { const m = metricMeta(col.metric); - const value = row ? row.metrics[col.metric] ?? null : a?.metrics?.[col.metric] ?? null; out.push({ label: col.label || m.label, - value, + value: a?.metrics?.[col.metric] ?? null, unit: col.unit || m.unit, color: colorForMetric(col.metric), - // Prefer the selected service's spark; otherwise reuse the - // layer-aggregate series. - spark: - row?.spark && row.spark.length > 1 - ? row.spark - : a?.seriesByMetric?.[col.metric], + spark: a?.seriesByMetric?.[col.metric], }); } return out; }); -// Source/level chip text (mirrors design's "from Java agent · OAP v10.3"). -const sourceText = computed(() => { - if (!layer.value) return ''; - const lvl = layer.value.level; - return lvl !== null && lvl !== undefined ? `OAP layer · level ${lvl}` : 'OAP layer'; -}); </script> <template> @@ -180,8 +165,6 @@ const sourceText = computed(() => { <div class="identity-text"> <div class="title-row"> <h1>{{ displayName }}</h1> - <span class="sw-tag layer-tag">LAYER</span> - <span class="sw-tag">{{ sourceText }}</span> <span v-if="layer.serviceCount === 0" class="sw-badge warn">no services</span> <span v-else-if="!layer.active" class="sw-badge">no data</span> </div>
