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 1c5f8e4  overview: auto-built landing at /; sidebar follows landing 
priority
1c5f8e4 is described below

commit 1c5f8e4c92bd57422e448795c7ba67c4083cec29
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 12 14:31:51 2026 +0800

    overview: auto-built landing at /; sidebar follows landing priority
    
    - Split routes: / is now the Overview (cross-layer landing composed
      from each enabled layer's setup); /setup moves to its own route.
    - New composable useLandingOrder: shared priority-based sort used by
      the sidebar's Layers section AND the Overview cards so the two stay
      in lockstep. useLandingLayers filters to landing.enabled.
    - LayerLandingCard renders each enabled layer with its configured
      columns + topN as a placeholder table; live service data plugs in
      during Stage 2.4.
    - Sidebar adds a 'Setup' lead link beside Overview so operators bounce
      between the two during initial configuration.
    - Setup page now shares the same useLandingOrder so its ordering
      matches the sidebar/overview.
---
 apps/ui/src/components/shell/AppSidebar.vue     |  18 ++-
 apps/ui/src/composables/useLandingOrder.ts      |  53 +++++++
 apps/ui/src/router/index.ts                     |   6 +-
 apps/ui/src/views/overview/LayerLandingCard.vue | 188 ++++++++++++++++++++++++
 apps/ui/src/views/overview/OverviewView.vue     | 155 +++++++++++++++++++
 apps/ui/src/views/setup/SetupView.vue           |  16 +-
 6 files changed, 419 insertions(+), 17 deletions(-)

diff --git a/apps/ui/src/components/shell/AppSidebar.vue 
b/apps/ui/src/components/shell/AppSidebar.vue
index d09d05e..bd054ee 100644
--- a/apps/ui/src/components/shell/AppSidebar.vue
+++ b/apps/ui/src/components/shell/AppSidebar.vue
@@ -21,6 +21,7 @@ import Icon, { type IconName } from 
'@/components/icons/Icon.vue';
 import logoSw from '@/assets/icons/logo-sw.svg?raw';
 import { useAuthStore } from '@/stores/auth';
 import { useLayers } from '@/composables/useLayers';
+import { useLandingOrder } from '@/composables/useLandingOrder';
 
 const auth = useAuthStore();
 const router = useRouter();
@@ -30,13 +31,15 @@ async function signOut(): Promise<void> {
 }
 
 const { availableLayers, oapReachable, oapError, hasTopology } = useLayers();
+// Sidebar shares the landing's priority order so the two views stay in sync.
+const orderedLayers = useLandingOrder(availableLayers);
 
 // Default-open the first available layer once data arrives; user clicks
 // thereafter take over.
 const expandedLayer = ref<string | null>(null);
 let userTouched = false;
 watch(
-  availableLayers,
+  orderedLayers,
   (rows) => {
     if (userTouched || expandedLayer.value) return;
     if (rows.length > 0) expandedLayer.value = rows[0].key;
@@ -68,6 +71,10 @@ interface NavSection {
 // One leading row before the Layers block — the cross-layer landing.
 const overview: NavRow = { icon: 'dash', label: 'Overview', to: '/' };
 
+// Setup sits next to Overview as a leading link too — operators bounce
+// between these two during initial configuration.
+const setup: NavRow = { icon: 'set', label: 'Setup', to: '/setup' };
+
 // Vantage-style flat kickers for the Operate / Admin half of the sidebar.
 // Alarms is user-facing so it sits before the Operate block (between user
 // observability concerns and OAP operator concerns).
@@ -136,6 +143,13 @@ const sections: NavSection[] = [
       >
         <Icon :name="overview.icon" /><span>{{ overview.label }}</span>
       </RouterLink>
+      <RouterLink
+        :to="setup.to"
+        class="sw-nav-item lead"
+        :class="{ 'is-active': isActive(setup.to) }"
+      >
+        <Icon :name="setup.icon" /><span>{{ setup.label }}</span>
+      </RouterLink>
 
       <div class="sw-nav-section sw-row" style="justify-content: 
space-between">
         <span>Layers</span>
@@ -152,7 +166,7 @@ const sections: NavSection[] = [
           set up a layer
         </RouterLink>
       </div>
-      <template v-for="L in availableLayers" :key="L.key">
+      <template v-for="L in orderedLayers" :key="L.key">
         <div
           class="layer-row"
           :class="{ 'is-active': expandedLayer === L.key }"
diff --git a/apps/ui/src/composables/useLandingOrder.ts 
b/apps/ui/src/composables/useLandingOrder.ts
new file mode 100644
index 0000000..2f5f78f
--- /dev/null
+++ b/apps/ui/src/composables/useLandingOrder.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { computed, type ComputedRef } from 'vue';
+import type { LayerDef } from '@skywalking-horizon-ui/api-client';
+import { useSetupStore } from '@/stores/setup';
+
+/**
+ * Sort layers by `landing.priority` (lower first). Ties break by the OAP
+ * catalog order already present in `layers` (since the BFF returned them
+ * that way). Lazy-creates a default config for each layer the user hasn't
+ * touched, using `defaultPriority` baked into the setup store.
+ *
+ * Used by BOTH the Overview page (card order) and the sidebar's Layers
+ * section (row order) so the two stay in lockstep.
+ */
+export function useLandingOrder(layers: ComputedRef<readonly LayerDef[]>) {
+  const store = useSetupStore();
+  return computed<LayerDef[]>(() => {
+    return [...layers.value].sort((a, b) => {
+      const pa = store.ensure(a.key, { slots: a.slots, caps: a.caps 
}).landing.priority;
+      const pb = store.ensure(b.key, { slots: b.slots, caps: b.caps 
}).landing.priority;
+      if (pa !== pb) return pa - pb;
+      return 0; // preserve incoming catalog order
+    });
+  });
+}
+
+/** Layers that opted in to the landing (`landing.enabled === true`),
+ *  sorted by priority. Drives the Overview's card list. */
+export function useLandingLayers(layers: ComputedRef<readonly LayerDef[]>) {
+  const store = useSetupStore();
+  const ordered = useLandingOrder(layers);
+  return computed<LayerDef[]>(() =>
+    ordered.value.filter(
+      (L) => store.ensure(L.key, { slots: L.slots, caps: L.caps 
}).landing.enabled,
+    ),
+  );
+}
diff --git a/apps/ui/src/router/index.ts b/apps/ui/src/router/index.ts
index 0bdd479..9e9892c 100644
--- a/apps/ui/src/router/index.ts
+++ b/apps/ui/src/router/index.ts
@@ -65,10 +65,8 @@ function layerSubRoutes(): RouteRecordRaw[] {
 }
 
 const shellRoutes: RouteRecordRaw[] = [
-  { path: '', name: 'setup', component: () => 
import('@/views/setup/SetupView.vue') },
-  // The eventual landing route — wired live in Stage 2.4 once the layer
-  // landing data endpoint exists. For now it shares the setup component.
-  { path: 'landing', name: 'landing', component: () => 
import('@/views/setup/SetupView.vue') },
+  { path: '', name: 'overview', component: () => 
import('@/views/overview/OverviewView.vue') },
+  { path: 'setup', name: 'setup', component: () => 
import('@/views/setup/SetupView.vue') },
   ...layerSubRoutes(),
   // Alerts (user-facing — alarms are observability data, not operator-only)
   { path: 'alarms', component: placeholder, props: { title: 'Alarms', phase: 
'Phase 5', note: 'Read-only; recovery is backend-auto. Live debug card via 
admin REST.' } },
diff --git a/apps/ui/src/views/overview/LayerLandingCard.vue 
b/apps/ui/src/views/overview/LayerLandingCard.vue
new file mode 100644
index 0000000..9496e2e
--- /dev/null
+++ b/apps/ui/src/views/overview/LayerLandingCard.vue
@@ -0,0 +1,188 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<script setup lang="ts">
+import { computed } from 'vue';
+import { RouterLink } from 'vue-router';
+import type { LayerDef } from '@skywalking-horizon-ui/api-client';
+import Icon from '@/components/icons/Icon.vue';
+import { useSetupStore } from '@/stores/setup';
+
+const props = defineProps<{ layer: LayerDef }>();
+const store = useSetupStore();
+const cfg = computed(() => store.ensure(props.layer.key, { slots: 
props.layer.slots, caps: props.layer.caps }));
+const slotName = computed(() => cfg.value.slots.services ?? 'Services');
+const detailHref = computed(() => `/layer/${props.layer.key}`);
+</script>
+
+<template>
+  <section class="sw-card layer-landing">
+    <header class="head">
+      <span class="dot" :style="{ background: layer.color }" />
+      <div class="title-block">
+        <h2>
+          <RouterLink :to="detailHref">{{ cfg.displayName || layer.name 
}}</RouterLink>
+        </h2>
+        <div class="sub">
+          {{ layer.serviceCount }} {{ slotName.toLowerCase() }}
+          <span v-if="cfg.landing.priority !== undefined" class="sep">·</span>
+          <span class="kicker">priority {{ cfg.landing.priority }}</span>
+          <span class="sep">·</span>
+          <span class="kicker">top {{ cfg.landing.topN }} by {{ 
cfg.landing.orderBy }}</span>
+        </div>
+      </div>
+      <RouterLink class="sw-btn" :to="detailHref">
+        <span>View all</span>
+        <Icon name="chev" :size="10" />
+      </RouterLink>
+    </header>
+
+    <div class="body">
+      <table class="sw-table">
+        <thead>
+          <tr>
+            <th class="svc-col">{{ slotName }}</th>
+            <th v-for="c in cfg.landing.columns" :key="c.metric" class="num">
+              {{ c.label }}<span v-if="c.unit" class="unit">{{ c.unit }}</span>
+            </th>
+            <th v-if="cfg.landing.spark" class="spark-col">trend</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="i in cfg.landing.topN" :key="i" class="placeholder-row">
+            <td class="svc-col">
+              <span class="shim w-name" />
+            </td>
+            <td v-for="c in cfg.landing.columns" :key="c.metric" class="num">
+              <span class="shim w-num" />
+            </td>
+            <td v-if="cfg.landing.spark" class="spark-col">
+              <span class="shim w-spark" />
+            </td>
+          </tr>
+        </tbody>
+      </table>
+      <div class="card-foot">
+        <span class="placeholder-note">
+          Live service data lands in Stage 2.4 — <RouterLink 
to="/setup">customize this card</RouterLink>.
+        </span>
+      </div>
+    </div>
+  </section>
+</template>
+
+<style scoped>
+.layer-landing {
+  margin-bottom: 12px;
+}
+.head {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 12px 14px;
+  border-bottom: 1px solid var(--sw-line);
+}
+.head .dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  flex: 0 0 8px;
+}
+.title-block {
+  flex: 1;
+  min-width: 0;
+}
+h2 {
+  margin: 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--sw-fg-0);
+  letter-spacing: -0.01em;
+}
+h2 a {
+  color: inherit;
+  text-decoration: none;
+}
+h2 a:hover {
+  color: var(--sw-accent-2);
+}
+.sub {
+  font-size: 11px;
+  color: var(--sw-fg-2);
+  display: flex;
+  gap: 6px;
+  align-items: center;
+  flex-wrap: wrap;
+  margin-top: 2px;
+}
+.sub .kicker {
+  color: var(--sw-fg-3);
+}
+.sub .sep {
+  color: var(--sw-fg-3);
+  opacity: 0.5;
+}
+.head .sw-btn {
+  height: 26px;
+  font-size: 11px;
+  text-decoration: none;
+}
+.body {
+  padding: 4px 0 8px;
+}
+.svc-col {
+  width: 28%;
+  min-width: 160px;
+}
+.spark-col {
+  width: 80px;
+}
+th .unit {
+  margin-left: 3px;
+  color: var(--sw-fg-3);
+  font-weight: 400;
+}
+td.num {
+  font-variant-numeric: tabular-nums;
+  text-align: right;
+}
+.placeholder-row .shim {
+  display: inline-block;
+  height: 9px;
+  background: var(--sw-bg-3);
+  border-radius: 3px;
+}
+.placeholder-row .w-name {
+  width: 60%;
+}
+.placeholder-row .w-num {
+  width: 36px;
+}
+.placeholder-row .w-spark {
+  width: 64px;
+  height: 14px;
+}
+.card-foot {
+  padding: 8px 14px 4px;
+  border-top: 1px dashed var(--sw-line);
+  font-size: 10.5px;
+  color: var(--sw-fg-3);
+}
+.placeholder-note a {
+  color: var(--sw-accent-2);
+  text-decoration: none;
+}
+</style>
diff --git a/apps/ui/src/views/overview/OverviewView.vue 
b/apps/ui/src/views/overview/OverviewView.vue
new file mode 100644
index 0000000..e8df152
--- /dev/null
+++ b/apps/ui/src/views/overview/OverviewView.vue
@@ -0,0 +1,155 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<script setup lang="ts">
+import { computed } from 'vue';
+import { RouterLink } from 'vue-router';
+import { useLayers } from '@/composables/useLayers';
+import { useLandingLayers } from '@/composables/useLandingOrder';
+import LayerLandingCard from './LayerLandingCard.vue';
+
+const { availableLayers, oapReachable, oapError, isLoading } = useLayers();
+const enabledLayers = useLandingLayers(availableLayers);
+
+// "No one opted in yet" → guide the operator to Setup before anything
+// useful can render.
+const empty = computed(() => !isLoading.value && enabledLayers.value.length 
=== 0);
+</script>
+
+<template>
+  <div class="overview">
+    <header class="page-head">
+      <div>
+        <div class="kicker">Overview</div>
+        <h1>Cross-layer landing</h1>
+        <p class="lede">
+          Auto-built from the layers you've enabled in
+          <RouterLink to="/setup">Setup</RouterLink>, in the order each 
layer's priority defines.
+          Each card shows the top services for that layer with its configured 
metrics.
+        </p>
+      </div>
+    </header>
+
+    <div v-if="!oapReachable && !isLoading" class="banner err">
+      <strong>OAP unreachable.</strong>
+      {{ oapError ?? 'Check that the OAP query host is up and reachable from 
the BFF.' }}
+    </div>
+
+    <div v-if="empty" class="empty">
+      <div class="empty-card">
+        <h2>Nothing on the landing yet</h2>
+        <p v-if="availableLayers.length === 0">
+          No layer is reporting services right now. Once data starts flowing 
through OAP, the
+          layers appear in <RouterLink to="/setup">Setup</RouterLink> for you 
to enable here.
+        </p>
+        <p v-else>
+          {{ availableLayers.length }} layer{{ availableLayers.length === 1 ? 
'' : 's' }} reporting,
+          none enabled on the landing yet. Open <RouterLink 
to="/setup">Setup</RouterLink>, toggle
+          "Show this layer on the landing" for the ones you care about, and 
they'll appear here in
+          priority order.
+        </p>
+        <RouterLink class="sw-btn is-primary" to="/setup">
+          Open Setup
+        </RouterLink>
+      </div>
+    </div>
+
+    <div v-else class="cards">
+      <LayerLandingCard v-for="L in enabledLayers" :key="L.key" :layer="L" />
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.overview {
+  padding: 20px 20px 60px;
+  max-width: 1140px;
+  margin: 0 auto;
+}
+.page-head {
+  margin-bottom: 18px;
+}
+.kicker {
+  font-size: 10px;
+  text-transform: uppercase;
+  letter-spacing: 0.1em;
+  color: var(--sw-accent);
+  margin-bottom: 6px;
+}
+.page-head h1 {
+  font-size: 22px;
+  font-weight: 600;
+  letter-spacing: -0.02em;
+  color: var(--sw-fg-0);
+  margin: 0 0 8px;
+}
+.lede {
+  font-size: 12.5px;
+  color: var(--sw-fg-1);
+  line-height: 1.5;
+  margin: 0;
+  max-width: 720px;
+}
+.lede a {
+  color: var(--sw-accent-2);
+  text-decoration: none;
+}
+.banner.err {
+  margin: 0 0 16px;
+  padding: 10px 12px;
+  background: var(--sw-err-soft);
+  border: 1px solid rgba(239, 68, 68, 0.3);
+  border-radius: 6px;
+  color: #f87171;
+  font-size: 12px;
+  line-height: 1.5;
+}
+.empty {
+  margin-top: 20px;
+}
+.empty-card {
+  background: var(--sw-bg-1);
+  border: 1px dashed var(--sw-line-2);
+  border-radius: 10px;
+  padding: 28px;
+  text-align: center;
+  max-width: 600px;
+  margin: 0 auto;
+}
+.empty-card h2 {
+  font-size: 15px;
+  color: var(--sw-fg-0);
+  margin: 0 0 8px;
+}
+.empty-card p {
+  font-size: 12px;
+  color: var(--sw-fg-2);
+  margin: 0 0 16px;
+  line-height: 1.5;
+}
+.empty-card .sw-btn {
+  display: inline-flex;
+  text-decoration: none;
+}
+.empty-card a {
+  color: var(--sw-accent-2);
+  text-decoration: none;
+}
+.cards {
+  display: flex;
+  flex-direction: column;
+}
+</style>
diff --git a/apps/ui/src/views/setup/SetupView.vue 
b/apps/ui/src/views/setup/SetupView.vue
index 6cea498..eff3230 100644
--- a/apps/ui/src/views/setup/SetupView.vue
+++ b/apps/ui/src/views/setup/SetupView.vue
@@ -18,22 +18,16 @@
 import { computed, ref } from 'vue';
 import LayerSetupCard from './LayerSetupCard.vue';
 import { useLayers } from '@/composables/useLayers';
+import { useLandingOrder } from '@/composables/useLandingOrder';
 import { useSetupStore } from '@/stores/setup';
 
 const { layers, oapReachable, oapError, isLoading } = useLayers();
 const store = useSetupStore();
 
-// Order by priority (lower first), with active layers always above inactive at
-// the same priority. The layer order on the landing will mirror this.
-const orderedLayers = computed(() =>
-  [...layers.value].sort((a, b) => {
-    const pa = store.ensure(a.key, { slots: a.slots, caps: a.caps 
}).landing.priority;
-    const pb = store.ensure(b.key, { slots: b.slots, caps: b.caps 
}).landing.priority;
-    if (pa !== pb) return pa - pb;
-    if (a.active !== b.active) return a.active ? -1 : 1;
-    return a.name.localeCompare(b.name);
-  }),
-);
+// Order by priority (lower first) so this page lines up with the sidebar
+// and the Overview. Setup shows ALL layers (active or not) — operators
+// configure layers ahead of receivers coming online.
+const orderedLayers = useLandingOrder(layers);
 
 const enabledOnLanding = computed(() =>
   orderedLayers.value.filter((L) => store.ensure(L.key, { slots: L.slots, 
caps: L.caps }).landing.enabled),

Reply via email to