You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/05/17 03:27:47 UTC
(skywalking-horizon-ui) 02/02: Revert "ui sidebar: collapse duplicated layer-tab blocks + memoize nav per layer"
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 <wu...@foxmail.com>
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>