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;
