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

wu-sheng pushed a commit to branch fix/editor-source-default-remote
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git

commit cff010acda56768ec76fa55ab2fafd42f8af87c1
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 26 21:24:06 2026 +0800

    fix(admin): default editor to remote + reset truly discards local draft
    
    Two operator-facing inconsistencies on the Layer Dashboards and
    Overview Templates admin editors:
    
    1. **Editor source default = remote.** Remote is what `pickLayerContent`
       / `pickOverviewContent` in bundle.ts serve to end users for synced
       / diverged / remote-only rows. The editor now matches: on every
       (re-)mount it loads remote whenever reachable (local draft still
       wins when present; falls through to bundled only on
       bundled-fallback). The `from {source}` pill renders only when the
       operator has stepped off the default (saved a LOCAL draft, or hit
       Reset to BUNDLED) — labelling the default state was noise that
       diverged between the two pages (Layer Dashboards showed
       "from remote", Overview Templates showed nothing).
    
    2. **Reset really discards.** The dropdown items "Reset to bundled" /
       "Reset to remote" are tooltipped "Discard current edits and reload
       …", but the implementation called `persistLocal(new_content)` after
       `loadFrom(src)`. On a diverged row that wrote the freshly-loaded
       content into localEdits as a brand-new draft — so right after
       hitting reset the TemplateConflictPrompt would pop up
       ("You have unpublished local edits · Layer · ActiveMQ"). Reset now
       calls `localEdits.remove(editName)` instead; subsequent edits in
       the editor re-create a draft naturally on the next change.
    
    Adds the new 'from {source}' + 'local' keys to all eight locale
    catalogs ('remote' and 'bundled' were already there).
---
 .../admin/layer-templates/LayerDashboardsAdmin.vue | 54 ++++++++++++-------
 .../overview-templates/OverviewTemplatesAdmin.vue  | 63 ++++++++++------------
 apps/ui/src/i18n/locales/de.json                   |  4 +-
 apps/ui/src/i18n/locales/en.json                   |  4 +-
 apps/ui/src/i18n/locales/es.json                   |  4 +-
 apps/ui/src/i18n/locales/fr.json                   |  4 +-
 apps/ui/src/i18n/locales/ja.json                   |  4 +-
 apps/ui/src/i18n/locales/ko.json                   |  4 +-
 apps/ui/src/i18n/locales/pt.json                   |  4 +-
 apps/ui/src/i18n/locales/zh-CN.json                |  4 +-
 10 files changed, 87 insertions(+), 62 deletions(-)

diff --git 
a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue 
b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
index 3cd07d6..c65d270 100644
--- a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
+++ b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
@@ -253,7 +253,14 @@ watch(
  * is the baseline for `dirty` (= unsaved edits since the last load/save).
  * Saving always writes LOCAL; you then publish LOCAL → OAP.
  * ─────────────────────────────────────────────────────────────────── */
-const editorSource = ref<'local' | 'bundled' | 'remote'>('bundled');
+// Remote is the canonical baseline (what the runtime menu renders for
+// synced + diverged + remote-only rows). The editor opens from remote
+// on every (re-)mount and stays there until the operator explicitly
+// loads bundled (Reset to bundled) or saves a local draft. The default
+// `'remote'` here matches what `syncDraft()` will pick on the first
+// pass — pre-seeding the same value lets the source pill stay hidden
+// (labelling the default would be noise).
+const editorSource = ref<'local' | 'bundled' | 'remote'>('remote');
 const loadedSnapshot = ref<string>('');
 const editName = computed(() => layerEditName(selectedKey.value));
 const hasLocalDraft = computed(() => localEdits.has(editName.value));
@@ -293,22 +300,22 @@ function loadFrom(src: 'local' | 'bundled' | 'remote'): 
void {
   editorSource.value = src;
   saveMsg.value = null;
 }
-/** Seed the editor when the selected layer changes. Priority mirrors
- *  what the operator actually sees in the live menu / dashboards:
+/** Seed the editor when the selected layer changes. Remote is the
+ *  canonical baseline — it's what `pickLayerContent` in the runtime
+ *  bundle serves to end users for synced / diverged / remote-only
+ *  rows, so the editor opens from remote whenever remote is reachable.
+ *  Priority:
  *    1. Local draft — unpublished in-progress edits in this browser.
- *    2. Remote — when bundled and remote diverged (or the layer is
- *       remote-only), OAP is the source of truth and the runtime
- *       bundle loads it via `pickLayerContent` (bundle.ts). Showing
- *       bundled here would silently disagree with the live UI.
- *    3. Bundled — synced rows are byte-equal anyway; bundled-fallback
- *       is the bundle's only source. */
+ *    2. Remote — the default for every re-mount when remote exists.
+ *    3. Bundled — only when remote is absent (bundled-fallback). The
+ *       operator can also hit "Reset to bundled" explicitly to swap;
+ *       that path goes through `resetTo`, not this seed function. */
 function syncDraft(): void {
   if (hasLocalDraft.value) {
     loadFrom('local');
     return;
   }
-  const badge = sync.badgeFor(editName.value);
-  if ((badge === 'diverged' || badge === 'remote-only') && 
remoteAvailable.value) {
+  if (remoteAvailable.value) {
     loadFrom('remote');
     return;
   }
@@ -329,12 +336,17 @@ function persistLocal(content: AdminLayerTemplate): void {
   loadedSnapshot.value = JSON.stringify(content);
 }
 
-// "Reset to ▾" dropdown — reload the editor from a source AND adopt it as
-// the local draft (so the choice persists + Push/diff status updates).
+// "Reset to ▾" dropdown — discard the current local draft and reload the
+// editor from the picked source. The op-facing tooltip says "Discard
+// current edits and reload …", so the action must DROP the local draft
+// rather than re-stage the new content as a fresh draft (which the
+// previous implementation did via persistLocal, triggering the
+// TemplateConflictPrompt right after every reset). Subsequent edits in
+// the editor re-create a local draft naturally on the next change.
 const resetDropdownOpen = ref(false);
 function resetTo(src: 'bundled' | 'remote'): void {
   loadFrom(src);
-  if (draft.template) persistLocal(draft.template);
+  localEdits.remove(editName.value);
   resetDropdownOpen.value = false;
 }
 
@@ -1599,13 +1611,17 @@ const namingTest = computed<NamingTestResult>(() => {
             </div>
             <div class="actions">
               <span v-if="saveMsg" class="save-msg">{{ saveMsg }}</span>
-              <!-- Where the editor content was loaded from — distinct from
-                   the sync-status chip by the title (prefixed "from "). -->
+              <!-- Source pill. Remote is the canonical default — the
+                   editor opens from remote on every (re-)mount, so a
+                   "from remote" label would be noise on the common
+                   path. Render only when the operator has stepped off
+                   the default (loaded their LOCAL draft, or hit Reset
+                   to BUNDLED). -->
               <span
-                v-if="editorSource === 'local' || !isSynced"
+                v-if="editorSource === 'local' || editorSource === 'bundled'"
                 class="src-tag"
-                :title="`Editing from: ${editorSource}`"
-              >from {{ editorSource }}</span>
+                :title="`Editing from: ${t(editorSource)}`"
+              >{{ t('from {source}', { source: t(editorSource) }) }}</span>
               <!-- Reset the editor to a source (discard current content). -->
               <div class="reset-dd">
                 <button class="sw-btn" type="button" @click="resetDropdownOpen 
= !resetDropdownOpen">
diff --git 
a/apps/ui/src/features/admin/overview-templates/OverviewTemplatesAdmin.vue 
b/apps/ui/src/features/admin/overview-templates/OverviewTemplatesAdmin.vue
index 353665e..c17236c 100644
--- a/apps/ui/src/features/admin/overview-templates/OverviewTemplatesAdmin.vue
+++ b/apps/ui/src/features/admin/overview-templates/OverviewTemplatesAdmin.vue
@@ -553,7 +553,13 @@ const isSynced = computed<boolean>(
 );
 
 const draft = ref<OverviewDashboard | null>(null);
-const editorSource = ref<'local' | 'bundled' | 'remote'>('bundled');
+// Remote is the canonical baseline. The editor opens from remote on
+// every (re-)mount (`defaultEditorSource` returns 'remote' whenever
+// remote is reachable + no local draft exists) and stays there until
+// the operator explicitly hits "Reset to bundled" or saves a local
+// draft. The initial ref matches so the source pill can stay hidden
+// on the default path.
+const editorSource = ref<'local' | 'bundled' | 'remote'>('remote');
 const loadedSnapshot = ref<string>('');
 
 function bundledContent(): OverviewDashboard | null {
@@ -587,12 +593,15 @@ function persistLocal(content: OverviewDashboard): void {
   loadedSnapshot.value = JSON.stringify(content);
 }
 
-// "Reset to ▾" dropdown — reload from a source AND adopt it as the local
-// draft (so the choice persists + Push/diff status updates).
+// "Reset to ▾" dropdown — discard the current local draft and reload
+// the editor from the picked source. Symmetric with the layer
+// dashboards editor: reset is "discard, reload", not "re-stage as a
+// new draft" (which would trigger TemplateConflictPrompt right after
+// every reset). Subsequent edits re-create a local draft naturally.
 const resetDropdownOpen = ref(false);
 function resetTo(src: 'bundled' | 'remote'): void {
   loadFrom(src);
-  if (draft.value) persistLocal(draft.value);
+  if (editName.value) localEdits.remove(editName.value);
   resetDropdownOpen.value = false;
 }
 
@@ -667,25 +676,16 @@ const isDirty = computed<boolean>(() =>
 );
 
 // Which source to seed the editor from for the current selection.
-// Priority mirrors what the operator sees on the live overview pages:
+// Remote is the canonical baseline — it's what `pickOverviewContent`
+// in the runtime bundle serves to end users — so the editor opens
+// from remote on every re-mount when remote is reachable. Priority:
 //   1. Local draft — unpublished in-progress edits in this browser.
-//   2. Remote — when bundled and remote diverged (or the row is
-//      remote-only), OAP is the source of truth and the runtime bundle
-//      loads remote via `pickOverviewContent` (bundle.ts). Loading
-//      bundled here silently disagrees with the live UI.
-//   3. Bundled — synced rows are byte-equal anyway; bundled-fallback is
-//      the bundle's only source.
-//   4. Last resort: remote when there's no bundled at all
-//      (defensive — every overview ships a bundled default today).
+//   2. Remote — the default for every re-entry when remote exists.
+//   3. Bundled — fallback for bundled-only overviews (no OAP row).
 function defaultEditorSource(): 'local' | 'bundled' | 'remote' {
   if (hasLocalDraft.value) return 'local';
-  if (editorSource.value === 'remote' && remoteAvailable.value) return 
'remote';
-  const badge = editName.value ? sync.badgeFor(editName.value) : null;
-  if ((badge === 'diverged' || badge === 'remote-only') && 
remoteAvailable.value) {
-    return 'remote';
-  }
-  if (bundledContent()) return 'bundled';
   if (remoteAvailable.value) return 'remote';
+  if (bundledContent()) return 'bundled';
   return 'bundled';
 }
 
@@ -1058,24 +1058,17 @@ function widgetKindLabel(type: OverviewWidget['type']): 
string {
             <!-- Source / save / publish actions, right-aligned (same row as
                  the title + tabs, mirroring the layer dashboards editor). -->
             <div class="ot__head-actions">
-              <!-- Source pill. Mirrors the layer dashboards editor wording
-                   ("from <source>") so operators get the same hint about
-                   which bytes are currently in the buffer:
-                     - local   = unpublished browser draft
-                     - remote  = OAP-live (default for diverged / remote-
-                                 only rows so the editor agrees with what
-                                 the live menu renders)
-                     - bundled = shipped default (only when the row is
-                                 bundled-fallback or the operator just
-                                 hit "Reset to bundled")
-                   Hidden when the row is "synced" (bundled === remote) and
-                   no local draft exists, because in that case "from
-                   anything" is unambiguous noise. -->
+              <!-- Source pill. Remote is the canonical default — the
+                   editor opens from remote on every (re-)mount, so a
+                   "from remote" label would be noise on the common
+                   path. Render only when the operator has stepped off
+                   the default (loaded their LOCAL draft, or hit Reset
+                   to BUNDLED). -->
               <span
-                v-if="editorSource === 'local' || !isSynced"
+                v-if="editorSource === 'local' || editorSource === 'bundled'"
                 class="ot__src"
-                :title="`Editing from: ${editorSource}`"
-              >from {{ editorSource }}</span>
+                :title="`Editing from: ${t(editorSource)}`"
+              >{{ t('from {source}', { source: t(editorSource) }) }}</span>
               <div class="reset-dd">
                 <button type="button" class="ot__btn" 
@click="resetDropdownOpen = !resetDropdownOpen">
                   reset to <span class="caret" :class="{ open: 
resetDropdownOpen }">›</span>
diff --git a/apps/ui/src/i18n/locales/de.json b/apps/ui/src/i18n/locales/de.json
index aef836c..c83264d 100644
--- a/apps/ui/src/i18n/locales/de.json
+++ b/apps/ui/src/i18n/locales/de.json
@@ -1157,5 +1157,7 @@
   "Window exceeds {h}h cap": "Fenster überschreitet Obergrenze von {h} h",
   "last {window}": "letzte {window}",
   "Metrics Analysis Language - …": "Metrics Analysis Language - …",
-  "Operate · OAP configuration": "Betrieb · OAP-Konfiguration"
+  "Operate · OAP configuration": "Betrieb · OAP-Konfiguration",
+  "from {source}": "aus {source}",
+  "local": "lokal"
 }
diff --git a/apps/ui/src/i18n/locales/en.json b/apps/ui/src/i18n/locales/en.json
index 3cc5141..fa35d34 100644
--- a/apps/ui/src/i18n/locales/en.json
+++ b/apps/ui/src/i18n/locales/en.json
@@ -1157,5 +1157,7 @@
   "Window exceeds {h}h cap": "Window exceeds {h}h cap",
   "last {window}": "last {window}",
   "Metrics Analysis Language - …": "Metrics Analysis Language - …",
-  "Operate · OAP configuration": "Operate · OAP configuration"
+  "Operate · OAP configuration": "Operate · OAP configuration",
+  "from {source}": "from {source}",
+  "local": "local"
 }
diff --git a/apps/ui/src/i18n/locales/es.json b/apps/ui/src/i18n/locales/es.json
index 7f9a23d..9cd38d7 100644
--- a/apps/ui/src/i18n/locales/es.json
+++ b/apps/ui/src/i18n/locales/es.json
@@ -1157,5 +1157,7 @@
   "Window exceeds {h}h cap": "La ventana supera el tope de {h}h",
   "last {window}": "últimos {window}",
   "Metrics Analysis Language - …": "Metrics Analysis Language - …",
-  "Operate · OAP configuration": "Operar · Configuración de OAP"
+  "Operate · OAP configuration": "Operar · Configuración de OAP",
+  "from {source}": "desde {source}",
+  "local": "local"
 }
diff --git a/apps/ui/src/i18n/locales/fr.json b/apps/ui/src/i18n/locales/fr.json
index 5471b28..a1585b7 100644
--- a/apps/ui/src/i18n/locales/fr.json
+++ b/apps/ui/src/i18n/locales/fr.json
@@ -1157,5 +1157,7 @@
   "Window exceeds {h}h cap": "La fenêtre dépasse le plafond de {h} h",
   "last {window}": "dernier(s) {window}",
   "Metrics Analysis Language - …": "Metrics Analysis Language - …",
-  "Operate · OAP configuration": "Exploitation · Configuration OAP"
+  "Operate · OAP configuration": "Exploitation · Configuration OAP",
+  "from {source}": "depuis {source}",
+  "local": "local"
 }
diff --git a/apps/ui/src/i18n/locales/ja.json b/apps/ui/src/i18n/locales/ja.json
index 0864c8a..1723d61 100644
--- a/apps/ui/src/i18n/locales/ja.json
+++ b/apps/ui/src/i18n/locales/ja.json
@@ -1157,5 +1157,7 @@
   "Window exceeds {h}h cap": "範囲が {h} 時間の上限を超えています",
   "last {window}": "直近 {window}",
   "Metrics Analysis Language - …": "Metrics Analysis Language - …",
-  "Operate · OAP configuration": "運用 · OAP 設定"
+  "Operate · OAP configuration": "運用 · OAP 設定",
+  "from {source}": "{source} から",
+  "local": "ローカル"
 }
diff --git a/apps/ui/src/i18n/locales/ko.json b/apps/ui/src/i18n/locales/ko.json
index 3d08e58..c73ae71 100644
--- a/apps/ui/src/i18n/locales/ko.json
+++ b/apps/ui/src/i18n/locales/ko.json
@@ -1157,5 +1157,7 @@
   "Window exceeds {h}h cap": "범위가 {h}시간 상한을 초과합니다",
   "last {window}": "최근 {window}",
   "Metrics Analysis Language - …": "Metrics Analysis Language - …",
-  "Operate · OAP configuration": "운영 · OAP 구성"
+  "Operate · OAP configuration": "운영 · OAP 구성",
+  "from {source}": "{source}에서",
+  "local": "로컬"
 }
diff --git a/apps/ui/src/i18n/locales/pt.json b/apps/ui/src/i18n/locales/pt.json
index 52a347a..3e3a99a 100644
--- a/apps/ui/src/i18n/locales/pt.json
+++ b/apps/ui/src/i18n/locales/pt.json
@@ -1157,5 +1157,7 @@
   "Window exceeds {h}h cap": "A janela excede o limite de {h}h",
   "last {window}": "últimos {window}",
   "Metrics Analysis Language - …": "Metrics Analysis Language - …",
-  "Operate · OAP configuration": "Operar · Configuração do OAP"
+  "Operate · OAP configuration": "Operar · Configuração do OAP",
+  "from {source}": "a partir de {source}",
+  "local": "local"
 }
diff --git a/apps/ui/src/i18n/locales/zh-CN.json 
b/apps/ui/src/i18n/locales/zh-CN.json
index 86ce28c..e86ec24 100644
--- a/apps/ui/src/i18n/locales/zh-CN.json
+++ b/apps/ui/src/i18n/locales/zh-CN.json
@@ -1157,5 +1157,7 @@
   "Window exceeds {h}h cap": "窗口超过 {h} 小时上限",
   "last {window}": "最近 {window}",
   "Metrics Analysis Language - …": "Metrics Analysis Language - …",
-  "Operate · OAP configuration": "运维 · OAP 配置"
+  "Operate · OAP configuration": "运维 · OAP 配置",
+  "from {source}": "来自 {source}",
+  "local": "本地"
 }

Reply via email to