This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch feat/service-internal-topology
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git

commit 1460da5b885abe15e76f17d351fb1ba680d2b65e
Author: Wu Sheng <[email protected]>
AuthorDate: Tue Jun 9 13:31:22 2026 +0800

    feat(admin): make Layer dashboards layer-list-oriented
    
    The admin Layer dashboards picker listed only layers that ship a bundled
    JSON or live on OAP, so an OAP-reported layer with neither (e.g. a freshly
    monitored backend) couldn't be configured. Make the list layer-list
    oriented: merge the live layer roster (useLayers) with the loaded
    templates, so EVERY available layer appears. A layer with no template yet
    opens from a blank default (Service component on) that the operator can
    configure and Save — publishing the template to OAP on first save. No
    per-layer JSON has to be shipped for a layer to be configurable.
    
    - templates is now a computed union of rawTemplates (BFF) + synthesized
      blanks for roster layers not already present (blankTemplateFor).
    - syncDraft falls back to the bundled/synthesized template when a layer
      has no local draft and nothing on OAP, so untemplated layers open in
      the editor instead of a blank pane.
---
 CHANGELOG.md                                       |  6 ++
 .../admin/layer-templates/LayerDashboardsAdmin.vue | 66 ++++++++++++++++------
 2 files changed, 54 insertions(+), 18 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9342a39..3bd7d6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,12 @@ packages) plus the BFF's `HORIZON_VERSION` default.
   Service page). The previous hard-coded hidden-layer list (which dropped
   `BanyanDB`) is gone; a layer is only hidden when an admin explicitly
   disables its template.
+- **The admin Layer dashboards page is now layer-list-oriented.** It lists
+  every available layer — not just the ones shipping a bundled JSON or living
+  on OAP. A layer with no template yet opens on a blank default you can
+  configure (components, metric columns, widgets, topology) and **Save**,
+  which publishes the template to OAP on first save. No per-layer JSON has to
+  be shipped for a layer to be configurable.
 
 ### Dashboard widget visibility
 
diff --git 
a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue 
b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
index 8cc9f06..f9e5525 100644
--- a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
+++ b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
@@ -48,6 +48,7 @@ import type {
  *  live outside `DashboardScope` but surface as editable config tabs. */
 type AdminScope = DashboardScope | 'networkProfiling' | 
'serviceInternalTopology';
 import { bff, bffClient, BffApiError } from '@/api/client';
+import { useLayers } from '@/shell/useLayers';
 import { useLocalTemplateEdits, layerEditName } from 
'@/controls/localTemplateEdits';
 import { useTemplateSources } from 
'@/features/admin/_shared/useTemplateSources';
 import { buildExportEnvelope, downloadJson, pickJsonFile, validateImport } 
from '@/features/admin/_shared/templatePortability';
@@ -103,7 +104,33 @@ const SCOPE_LABELS: Record<AdminScope, string> = {
   networkProfiling: 'network profiling',
 };
 
-const templates = ref<AdminLayerTemplate[]>([]);
+// Templates loaded from the BFF (bundled + remote). The editable list
+// (`templates`) is LAYER-LIST oriented: it merges the loaded templates with
+// EVERY layer the live roster reports, so the picker shows all available
+// layers — not only the ones shipping a bundled JSON or living on OAP. A
+// layer with no template yet opens from a blank default and becomes real on
+// first Save (published to OAP).
+const rawTemplates = ref<AdminLayerTemplate[]>([]);
+const { layers: menuLayers } = useLayers();
+function blankTemplateFor(key: string, alias: string, color?: string): 
AdminLayerTemplate {
+  return {
+    key,
+    alias,
+    ...(color ? { color } : {}),
+    slots: {},
+    components: { service: true },
+    metrics: {},
+    widgets: [],
+  };
+}
+const templates = computed<AdminLayerTemplate[]>(() => {
+  const present = new Set(rawTemplates.value.map((t) => t.key.toUpperCase()));
+  const synthesized = menuLayers.value
+    .filter((L) => !present.has(L.key.toUpperCase()))
+    .map((L) => blankTemplateFor(L.key.toUpperCase(), L.name, L.color))
+    .sort((a, b) => a.key.localeCompare(b.key));
+  return [...rawTemplates.value, ...synthesized];
+});
 const isLoading = ref(true);
 const error = ref<string | null>(null);
 const selectedKey = ref<string>('');
@@ -187,6 +214,16 @@ function isDivergedRow(key: string): boolean {
   const s = sync.badgeFor(`horizon.layer.${key}`);
   return s === 'diverged' || s === 'bundled-fallback';
 }
+// Declared HERE — before divergedCount / localCount and their watches.
+// Those watches evaluate their source at setup, and `templates` can already
+// be non-empty (the merged live roster), so the filter callbacks read
+// `localEdits` synchronously — it must be initialized first (TDZ otherwise).
+const localEdits = useLocalTemplateEdits();
+// Server-side bundled + remote content for the Reset-to / Preview editor
+// sources. Local (browser draft) comes from `localEdits`.
+const sources = useTemplateSources('layer');
+const sourcesReady = computed(() => !sources.isLoading.value);
+const previewOverride = usePreviewOverride();
 const divergedCount = computed(() => templates.value.filter((t) => 
isDivergedRow(t.key)).length);
 const localCount = computed(() => templates.value.filter((t) => 
localEdits.has(layerEditName(t.key))).length);
 
@@ -212,28 +249,12 @@ const filteredTemplates = 
computed<AdminLayerTemplate[]>(() => {
  *  the Save / Reset state. */
 const draft = reactive<{ template: AdminLayerTemplate | null }>({ template: 
null });
 
-// Unpublished edits live in THIS browser (localStorage), not on the BFF
-// disk. The editor seeds from the browser draft when one exists, and the
-// config bundle overlays it on live pages — see controls/localTemplateEdits.
-const localEdits = useLocalTemplateEdits();
-// Server-side bundled + remote content for the Reset-to / Preview editor
-// sources. Local (browser draft) comes from `localEdits`.
-const sources = useTemplateSources('layer');
-// Settled-state flag for the source pill: until the config-bundle
-// fetch resolves, `hasRemote(name)` returns false for every name, so
-// any pill we render reflects the boot fallback (bundled) and then
-// flips to its real value when the bundle lands — a visual flash on
-// every page open. The pill v-if's on `sourcesReady` so it stays
-// hidden until the data is real.
-const sourcesReady = computed(() => !sources.isLoading.value);
-const previewOverride = usePreviewOverride();
-
 async function loadAll(): Promise<void> {
   isLoading.value = true;
   error.value = null;
   try {
     const res = await bffClient.layerTemplates.list();
-    templates.value = res.templates;
+    rawTemplates.value = res.templates;
     // Hydrate from `?layer=&scope=` first; fall back to the first
     // template only when the URL doesn't pin a layer. This preserves
     // refresh state.
@@ -362,6 +383,15 @@ function syncDraft(): void {
     loadFrom('remote');
     return;
   }
+  // No local draft and nothing on OAP — fall back to the bundled/synthesized
+  // template so layers that ship no JSON (an untemplated OAP layer like
+  // BanyanDB, surfaced from the live roster) still open in a blank editor
+  // the operator can configure and Save (publishing creates the OAP copy).
+  // `bundledContent()` resolves the synthesized blank via the templates list.
+  if (bundledContent()) {
+    loadFrom('bundled');
+    return;
+  }
   draft.template = null;
   loadedSnapshot.value = '';
   saveMsg.value = null;

Reply via email to