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 3c6f6c06a9bb9ff4e27f66208b008239ddef59ef Author: Wu Sheng <[email protected]> AuthorDate: Sun May 17 11:27:26 2026 +0800 Revert "ui sidebar: collapse duplicated layer-tab blocks + memoize nav per layer" This reverts commit c3544de5ae3e1d2c5fd0fd435b66346724955990. --- apps/ui/src/shell/AppSidebar.vue | 414 +++++++++++++++++++++++++-------------- 1 file changed, 264 insertions(+), 150 deletions(-) diff --git a/apps/ui/src/shell/AppSidebar.vue b/apps/ui/src/shell/AppSidebar.vue index 94194ce..b3661dd 100644 --- a/apps/ui/src/shell/AppSidebar.vue +++ b/apps/ui/src/shell/AppSidebar.vue @@ -20,7 +20,7 @@ import { RouterLink, useRoute, useRouter } from 'vue-router'; import Icon, { type IconName } from '@/components/icons/Icon.vue'; import logoSw from '@/assets/icons/logo-sw.svg?raw'; import { useAuthStore } from '@/state/auth'; -import { useLayers } from '@/shell/useLayers'; +import { useLayers, firstLayerTab } from '@/shell/useLayers'; import { useLandingOrder } from '@/shell/useLandingOrder'; import { useOverviewDashboards } from '@/render/overview/useOverviewDashboards'; @@ -31,7 +31,7 @@ async function signOut(): Promise<void> { await router.push({ name: 'login' }); } -const { availableLayers, oapReachable, oapError } = useLayers(); +const { availableLayers, oapReachable, oapError, hasTopology } = useLayers(); const orderedLayers = useLandingOrder(availableLayers); const { publicOverviews } = useOverviewDashboards(); @@ -41,97 +41,21 @@ function layerIcon(L: SidebarLayer): IconName { } type SidebarLayer = (typeof orderedLayers.value)[number]; - -/** - * Per-layer navigation descriptor. Built ONCE per layer set (rebuilt - * only when `orderedLayers` changes) and cached in `layerNavByKey`. - * Lets the template render each tab via a single `v-for` instead of - * the dozen inline `v-if cap` blocks that fired on every route change - * and made the sidebar feel sluggish on OAPs with many layers. - * - * `isSingle` — the layer collapses to a direct link (no accordion). - * `primaryTo` — `/layer/<key>/<firstLayerTab>` (used by the row click). - * `tabs` — child rows; empty when `isSingle`. - */ -interface LayerTab { - key: string; - icon: IconName; - label: string; - to: string; - /** Optional badge — currently only Service shows the service count. */ - badge?: number; +function hasInstances(L: SidebarLayer): boolean { + return L.caps.instances ?? Boolean(L.slots.instances); } -interface LayerNav { - isSingle: boolean; - primaryTo: string; - tabs: LayerTab[]; +function hasEndpoints(L: SidebarLayer): boolean { + return L.caps.endpoints ?? Boolean(L.slots.endpoints); } - -function buildLayerNav(L: SidebarLayer): LayerNav { +/** A layer whose only worthwhile screen is the services list — no + * tabs to expand into. Rendered as a direct link, not an accordion. */ +function isSingleFeatureLayer(L: SidebarLayer): boolean { + if (hasInstances(L) || hasEndpoints(L)) return false; + if (hasTopology(L)) return false; const c = L.caps; - const slots = L.slots; - const hasInstances = c.instances ?? Boolean(slots.instances); - const hasEndpoints = c.endpoints ?? Boolean(slots.endpoints); - const hasTopo = Boolean(c.serviceMap || c.instanceTopology || c.processTopology); - const hasAny = - hasInstances || - hasEndpoints || - hasTopo || - Boolean( - c.traces || - c.logs || - c.traceProfiling || - c.ebpfProfiling || - c.asyncProfiling || - c.networkProfiling || - c.pprofProfiling || - c.events || - c.endpointDependency, - ); - const firstTab = (() => { - if (c.dashboards) return 'service'; - if (hasInstances) return 'instance'; - if (hasEndpoints) return 'endpoint'; - if (hasTopo) return 'topology'; - if (c.endpointDependency) return 'dependency'; - if (c.traces) return 'trace'; - if (c.logs) return 'logs'; - if (c.traceProfiling) return 'trace-profiling'; - if (c.ebpfProfiling) return 'ebpf-profiling'; - if (c.networkProfiling) return 'network-profiling'; - if (c.asyncProfiling) return 'async-profiling'; - if (c.pprofProfiling) return 'pprof'; - return 'service'; - })(); - const primaryTo = `/layer/${L.key}/${firstTab}`; - const isSingle = !hasAny; - const tabs: LayerTab[] = []; - const push = (t: LayerTab) => tabs.push(t); - if (c.dashboards) push({ key: 'service', icon: 'svc', label: 'Service', to: `/layer/${L.key}/service`, badge: L.serviceCount }); - if (hasInstances) push({ key: 'instance', icon: 'prof', label: slots.instances ?? 'Instance', to: `/layer/${L.key}/instance` }); - if (hasEndpoints) push({ key: 'endpoint', icon: 'ep', label: slots.endpoints ?? 'Endpoint', to: `/layer/${L.key}/endpoint` }); - if (hasTopo) push({ key: 'topology', icon: 'topo', label: 'Topology', to: `/layer/${L.key}/topology` }); - if (c.endpointDependency) { - const label = slots.endpointDependency ?? `${slots.endpoints ?? 'Endpoint'} dependency`; - push({ key: 'dependency', icon: 'ep', label, to: `/layer/${L.key}/dependency` }); - } - if (c.traces) push({ key: 'trace', icon: 'trace', label: 'Traces', to: `/layer/${L.key}/trace` }); - if (c.logs) push({ key: 'logs', icon: 'log', label: 'Logs', to: `/layer/${L.key}/logs` }); - if (c.traceProfiling) push({ key: 'trace-profiling', icon: 'flame', label: 'Trace Profiling', to: `/layer/${L.key}/trace-profiling` }); - if (c.ebpfProfiling) push({ key: 'ebpf-profiling', icon: 'flame', label: 'eBPF Profiling', to: `/layer/${L.key}/ebpf-profiling` }); - if (c.networkProfiling) push({ key: 'network-profiling', icon: 'prof', label: 'Network Profiling', to: `/layer/${L.key}/network-profiling` }); - if (c.pprofProfiling) push({ key: 'pprof', icon: 'prof', label: 'pprof (Go)', to: `/layer/${L.key}/pprof` }); - if (c.asyncProfiling) push({ key: 'async-profiling', icon: 'flame', label: 'Async Profiling', to: `/layer/${L.key}/async-profiling` }); - return { isSingle, primaryTo, tabs }; -} - -const layerNavByKey = computed<Map<string, LayerNav>>(() => { - const m = new Map<string, LayerNav>(); - for (const L of orderedLayers.value) m.set(L.key, buildLayerNav(L)); - return m; -}); -function navFor(L: SidebarLayer): LayerNav { - return layerNavByKey.value.get(L.key) ?? buildLayerNav(L); + if (c.traces || c.logs || c.traceProfiling || c.ebpfProfiling || c.asyncProfiling || c.events) return false; + if (c.endpointDependency || c.serviceMap || c.instanceTopology || c.processTopology) return false; + return true; } const expandedLayer = ref<string | null>(null); @@ -139,11 +63,12 @@ function toggleLayer(key: string): void { const wasExpanded = expandedLayer.value === key; expandedLayer.value = wasExpanded ? null : key; if (!wasExpanded) { - const nav = layerNavByKey.value.get(key); - if (!nav) return; - if (route.path === nav.primaryTo) return; - if (route.path.startsWith(`/layer/${key}/`)) return; - void router.push(nav.primaryTo); + const L = orderedLayers.value.find((l) => l.key === key); + if (!L) return; + const target = `/layer/${L.key}/${firstLayerTab(L)}`; + if (route.path === target) return; + if (route.path.startsWith(`/layer/${L.key}/`)) return; + void router.push(target); } } @@ -352,52 +277,139 @@ watch( <span class="layer-group-name">{{ E.label }}</span> </div> <template v-for="L in E.layers" :key="`${E.label}::${L.key}`"> - <RouterLink - v-if="navFor(L).isSingle" - :to="navFor(L).primaryTo" - class="layer-row direct in-group" - :class="{ 'is-active': isActive(`/layer/${L.key}`) }" - > - <Icon :name="layerIcon(L)" /> - <span class="layer-name">{{ L.name }}</span> - </RouterLink> - <div - v-else - class="layer-row in-group" - :class="{ - 'is-expanded': expandedLayer === L.key, - 'is-active': isActiveExact(`/layer/${L.key}`), - }" - @click="toggleLayer(L.key)" - > - <Icon :name="layerIcon(L)" /> - <span class="layer-name">{{ L.name }}</span> - <span class="caret" :class="{ open: expandedLayer === L.key }"> - <Icon name="caret" :size="10" /> - </span> - </div> - <div - v-if="!navFor(L).isSingle && expandedLayer === L.key" - class="layer-children in-group" - > <RouterLink - v-for="tab in navFor(L).tabs" - :key="tab.key" - :to="tab.to" - class="sw-nav-item" - :class="{ 'is-active': isActive(tab.to) }" + v-if="isSingleFeatureLayer(L)" + :to="`/layer/${L.key}/${firstLayerTab(L)}`" + class="layer-row direct in-group" + :class="{ 'is-active': isActive(`/layer/${L.key}`) }" > - <Icon :name="tab.icon" /><span>{{ tab.label }}</span> - <span v-if="tab.badge != null" class="sw-badge" style="margin-left: auto">{{ tab.badge }}</span> + <Icon :name="layerIcon(L)" /> + <span class="layer-name">{{ L.name }}</span> </RouterLink> - </div> - </template> + <div + v-else + class="layer-row in-group" + :class="{ + 'is-expanded': expandedLayer === L.key, + 'is-active': isActiveExact(`/layer/${L.key}`), + }" + @click="toggleLayer(L.key)" + > + <Icon :name="layerIcon(L)" /> + <span class="layer-name">{{ L.name }}</span> + <span class="caret" :class="{ open: expandedLayer === L.key }"> + <Icon name="caret" :size="10" /> + </span> + </div> + <div + v-if="!isSingleFeatureLayer(L) && expandedLayer === L.key" + class="layer-children in-group" + > + <RouterLink + v-if="L.caps.dashboards" + :to="`/layer/${L.key}/${firstLayerTab(L)}`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/${firstLayerTab(L)}`) || route.path === `/layer/${L.key}` }" + > + <Icon name="svc" /><span>Service</span> + <span class="sw-badge" style="margin-left: auto">{{ L.serviceCount }}</span> + </RouterLink> + <RouterLink + v-if="hasInstances(L)" + :to="`/layer/${L.key}/instance`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/instance`) }" + > + <Icon name="prof" /><span>{{ L.slots.instances ?? 'Instance' }}</span> + </RouterLink> + <RouterLink + v-if="hasEndpoints(L)" + :to="`/layer/${L.key}/endpoint`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/endpoint`) }" + > + <Icon name="ep" /><span>{{ L.slots.endpoints ?? 'Endpoint' }}</span> + </RouterLink> + <RouterLink + v-if="hasTopology(L)" + :to="`/layer/${L.key}/topology`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/topology`) }" + > + <Icon name="topo" /><span>Topology</span> + </RouterLink> + <RouterLink + v-if="L.caps.endpointDependency" + :to="`/layer/${L.key}/dependency`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/dependency`) }" + > + <Icon name="ep" /><span>{{ L.slots.endpointDependency || 'Dependency' }}</span> + </RouterLink> + <RouterLink + v-if="L.caps.traces" + :to="`/layer/${L.key}/trace`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/trace`) }" + > + <Icon name="trace" /><span>Trace</span> + </RouterLink> + <RouterLink + v-if="L.caps.logs" + :to="`/layer/${L.key}/logs`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/logs`) }" + > + <Icon name="log" /><span>Logs</span> + </RouterLink> + <RouterLink + v-if="L.caps.traceProfiling" + :to="`/layer/${L.key}/trace-profiling`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/trace-profiling`) }" + > + <Icon name="prof" /><span>Trace Profiling</span> + </RouterLink> + <RouterLink + v-if="L.caps.ebpfProfiling" + :to="`/layer/${L.key}/ebpf-profiling`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/ebpf-profiling`) }" + > + <Icon name="prof" /><span>eBPF Profiling</span> + </RouterLink> + <RouterLink + v-if="L.caps.asyncProfiling" + :to="`/layer/${L.key}/async-profiling`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/async-profiling`) }" + > + <Icon name="prof" /><span>Async Profiling</span> + </RouterLink> + <RouterLink + v-if="L.caps.networkProfiling" + :to="`/layer/${L.key}/network-profiling`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/network-profiling`) }" + > + <Icon name="prof" /><span>Network Profiling</span> + </RouterLink> + <RouterLink + v-if="L.caps.pprofProfiling" + :to="`/layer/${L.key}/pprof`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/pprof`) }" + > + <Icon name="prof" /><span>pprof (Go)</span> + </RouterLink> + </div> + </template> </template> <!-- Ungrouped single-feature layer: direct link. --> <RouterLink - v-else-if="navFor(E.layer).isSingle" - :to="navFor(E.layer).primaryTo" + v-else-if="isSingleFeatureLayer(E.layer)" + :to="`/layer/${E.layer.key}/${firstLayerTab(E.layer)}`" class="layer-row direct" :class="{ 'is-active': isActive(`/layer/${E.layer.key}`) }" > @@ -421,18 +433,105 @@ watch( </span> </div> <div - v-if="E.kind === 'single' && !navFor(E.layer).isSingle && expandedLayer === E.layer.key" + v-if="E.kind === 'single' && !isSingleFeatureLayer(E.layer) && expandedLayer === E.layer.key" class="layer-children" > <RouterLink - v-for="tab in navFor(E.layer).tabs" - :key="tab.key" - :to="tab.to" + v-if="E.layer.caps.dashboards" + :to="`/layer/${E.layer.key}/${firstLayerTab(E.layer)}`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/${firstLayerTab(E.layer)}`) || route.path === `/layer/${E.layer.key}` }" + > + <Icon name="svc" /><span>Service</span> + <span class="sw-badge" style="margin-left: auto">{{ E.layer.serviceCount }}</span> + </RouterLink> + <RouterLink + v-if="hasInstances(E.layer)" + :to="`/layer/${E.layer.key}/instance`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/instance`) }" + > + <Icon name="prof" /><span>{{ E.layer.slots.instances ?? 'Instance' }}</span> + </RouterLink> + <RouterLink + v-if="hasEndpoints(E.layer)" + :to="`/layer/${E.layer.key}/endpoint`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/endpoint`) }" + > + <Icon name="ep" /><span>{{ E.layer.slots.endpoints ?? 'Endpoint' }}</span> + </RouterLink> + <RouterLink + v-if="hasTopology(E.layer)" + :to="`/layer/${E.layer.key}/topology`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/topology`) }" + > + <Icon name="topo" /><span>Topology</span> + </RouterLink> + <RouterLink + v-if="E.layer.caps.endpointDependency" + :to="`/layer/${E.layer.key}/dependency`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/dependency`) }" + > + <Icon name="ep" /><span>{{ E.layer.slots.endpointDependency ?? `${E.layer.slots.endpoints ?? 'Endpoint'} dependency` }}</span> + </RouterLink> + <RouterLink + v-if="E.layer.caps.traces" + :to="`/layer/${E.layer.key}/trace`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/trace`) }" + > + <Icon name="trace" /><span>Traces</span> + </RouterLink> + <RouterLink + v-if="E.layer.caps.logs" + :to="`/layer/${E.layer.key}/logs`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/logs`) }" + > + <Icon name="log" /><span>Logs</span> + </RouterLink> + <RouterLink + v-if="E.layer.caps.traceProfiling" + :to="`/layer/${E.layer.key}/trace-profiling`" class="sw-nav-item" - :class="{ 'is-active': isActive(tab.to) }" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/trace-profiling`) }" > - <Icon :name="tab.icon" /><span>{{ tab.label }}</span> - <span v-if="tab.badge != null" class="sw-badge" style="margin-left: auto">{{ tab.badge }}</span> + <Icon name="flame" /><span>Trace Profiling</span> + </RouterLink> + <RouterLink + v-if="E.layer.caps.ebpfProfiling" + :to="`/layer/${E.layer.key}/ebpf-profiling`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/ebpf-profiling`) }" + > + <Icon name="flame" /><span>eBPF Profiling</span> + </RouterLink> + <RouterLink + v-if="E.layer.caps.networkProfiling" + :to="`/layer/${E.layer.key}/network-profiling`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/network-profiling`) }" + > + <Icon name="prof" /><span>Network Profiling</span> + </RouterLink> + <RouterLink + v-if="E.layer.caps.pprofProfiling" + :to="`/layer/${E.layer.key}/pprof`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/pprof`) }" + > + <Icon name="prof" /><span>pprof (Go)</span> + </RouterLink> + <RouterLink + v-if="E.layer.caps.asyncProfiling" + :to="`/layer/${E.layer.key}/async-profiling`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${E.layer.key}/async-profiling`) }" + > + <Icon name="flame" /><span>Async Profiling</span> </RouterLink> </div> </template> @@ -444,8 +543,8 @@ watch( </div> <template v-for="L in operateLayers" :key="`op:${L.key}`"> <RouterLink - v-if="navFor(L).isSingle" - :to="navFor(L).primaryTo" + v-if="isSingleFeatureLayer(L)" + :to="`/layer/${L.key}/${firstLayerTab(L)}`" class="layer-row direct" :class="{ 'is-active': isActive(`/layer/${L.key}`) }" > @@ -469,18 +568,33 @@ watch( </span> </div> <div - v-if="!navFor(L).isSingle && expandedLayer === L.key" + v-if="!isSingleFeatureLayer(L) && expandedLayer === L.key" class="layer-children" > <RouterLink - v-for="tab in navFor(L).tabs" - :key="tab.key" - :to="tab.to" + v-if="L.caps.dashboards" + :to="`/layer/${L.key}/${firstLayerTab(L)}`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/${firstLayerTab(L)}`) || route.path === `/layer/${L.key}` }" + > + <Icon name="svc" /><span>Service</span> + <span class="sw-badge" style="margin-left: auto">{{ L.serviceCount }}</span> + </RouterLink> + <RouterLink + v-if="hasInstances(L)" + :to="`/layer/${L.key}/instance`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/layer/${L.key}/instance`) }" + > + <Icon name="prof" /><span>{{ L.slots.instances ?? 'Instance' }}</span> + </RouterLink> + <RouterLink + v-if="hasEndpoints(L)" + :to="`/layer/${L.key}/endpoint`" class="sw-nav-item" - :class="{ 'is-active': isActive(tab.to) }" + :class="{ 'is-active': isActive(`/layer/${L.key}/endpoint`) }" > - <Icon :name="tab.icon" /><span>{{ tab.label }}</span> - <span v-if="tab.badge != null" class="sw-badge" style="margin-left: auto">{{ tab.badge }}</span> + <Icon name="ep" /><span>{{ L.slots.endpoints ?? 'Endpoint' }}</span> </RouterLink> </div> </template>
