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 6d2caa16f484b52efde6ee8724047487adcbdfa0
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 12 23:05:32 2026 +0800

    admin: stack up/down/✕ on the right edge; scope-aware visibleWhen hints
    
    Widget editor controls — the up / down arrows and the delete button —
    used to live on opposite sides of each widget row with hand-tuned
    padding-top / margin-top values that drifted apart whenever the
    field rows below changed height. Move all three into one vertical
    stack on the right edge:
    
      .we-controls   — column flex, fixed 28x24px buttons, 4px gap.
                       Top aligned to the first input row (17px padding
                       above), delete sits at the bottom with a thin
                       divider so it reads as a separate destructive
                       action.
    
    VisibleWhen field gets scope-aware help:
      - Instance / Endpoint scopes: placeholder shows
        '#entity.jvm   or   instance_jvm_cpu has value', tooltip explains
        both predicate forms with #entity.<attr> as the entity-attribute
        variant.
      - Service / others: placeholder shows just the 'metric has value'
        form, tooltip notes that #entity.* predicates are mostly useful
        on Instance scope (service entities don't carry attributes).
---
 apps/ui/src/views/admin/LayerDashboardsAdmin.vue | 129 +++++++++++++++++------
 1 file changed, 96 insertions(+), 33 deletions(-)

diff --git a/apps/ui/src/views/admin/LayerDashboardsAdmin.vue 
b/apps/ui/src/views/admin/LayerDashboardsAdmin.vue
index 5829877..58b6d2e 100644
--- a/apps/ui/src/views/admin/LayerDashboardsAdmin.vue
+++ b/apps/ui/src/views/admin/LayerDashboardsAdmin.vue
@@ -255,6 +255,47 @@ function deleteMetricColumn(i: number): void {
   draft.template.metrics.columns.splice(i, 1);
 }
 
+/**
+ * Scope-aware `visibleWhen` placeholder + hover hint. Two supported
+ * predicate forms:
+ *
+ *   <metric> has value     The SPA evaluates this against the widget's
+ *                          own MQE result. If at least one bucket is
+ *                          non-null, the widget renders. Useful when
+ *                          the metric only exists for some services
+ *                          (e.g. service_mq_consume_count only on MQ
+ *                          producers).
+ *
+ *   #entity.<attr>         The SPA evaluates this against the *entity's*
+ *                          attributes. Only meaningful on scopes where
+ *                          entities carry attributes — i.e. INSTANCE
+ *                          (jvm / language / host etc.) and to a lesser
+ *                          extent ENDPOINT. Service-scope entities
+ *                          don't expose attributes, so the predicate is
+ *                          a no-op there.
+ *
+ * The placeholder / tooltip swap so operators editing an instance
+ * widget see entity-attribute syntax, and service-widget operators see
+ * the metric-has-value form.
+ */
+function visibleWhenPlaceholder(scope: DashboardScope): string {
+  if (scope === 'instance') return "#entity.jvm   or   instance_jvm_cpu has 
value";
+  if (scope === 'endpoint') return 'endpoint_cpm has value';
+  return 'service_mq_consume_count has value';
+}
+function visibleWhenHint(scope: DashboardScope): string {
+  const common =
+    'Hide the widget unless this predicate is truthy.\n' +
+    '\n' +
+    "  <metric> has value   — the widget's own MQE returned non-null data.";
+  const entityHint =
+    '\n  #entity.<attr>       — the active entity has the named attribute 
(e.g. #entity.jvm,\n' +
+    '                         #entity.language). Only meaningful for instance 
/ endpoint scopes;\n' +
+    "                         service-scope entities don't carry attributes.";
+  if (scope === 'instance' || scope === 'endpoint') return common + entityHint;
+  return common + '\n  (#entity.* predicates are entity-attribute-based and 
apply best on Instance scope.)';
+}
+
 /**
  * Component toggles surfaced in the admin editor. Each entry binds to
  * a key on the template's `components` block; flipping the toggle
@@ -493,22 +534,6 @@ function toggleComponent(key: ComponentKey): void {
           <ul v-else class="widget-list">
             <li v-for="(w, i) in currentWidgets" :key="i" class="widget-edit">
               <div class="we-row">
-                <div class="we-handle">
-                  <button
-                    class="sw-btn ghost small"
-                    type="button"
-                    :disabled="i === 0"
-                    title="Move up"
-                    @click="moveWidget(i, -1)"
-                  >↑</button>
-                  <button
-                    class="sw-btn ghost small"
-                    type="button"
-                    :disabled="i === currentWidgets.length - 1"
-                    title="Move down"
-                    @click="moveWidget(i, 1)"
-                  >↓</button>
-                </div>
                 <div class="we-fields">
                   <div class="row">
                     <label>
@@ -524,6 +549,7 @@ function toggleComponent(key: ComponentKey): void {
                       <select v-model="w.type">
                         <option value="card">card</option>
                         <option value="line">line</option>
+                        <option value="top">top</option>
                       </select>
                     </label>
                     <label>
@@ -540,12 +566,15 @@ function toggleComponent(key: ComponentKey): void {
                     </label>
                   </div>
                   <div class="row">
-                    <label class="grow wide">
+                    <label
+                      class="grow wide"
+                      :title="visibleWhenHint(activeScope)"
+                    >
                       <span>Visible when (optional)</span>
                       <input
                         class="mono"
                         v-model="w.visibleWhen"
-                        placeholder="#entity.jvm   or   service_jvm_cpu has 
value"
+                        :placeholder="visibleWhenPlaceholder(activeScope)"
                       />
                     </label>
                   </div>
@@ -561,9 +590,31 @@ function toggleComponent(key: ComponentKey): void {
                     </label>
                   </div>
                 </div>
-                <button class="sw-btn danger" type="button" title="Delete" 
@click="deleteWidget(i)">
-                  ✕
-                </button>
+                <!-- All three row controls live in a single vertical
+                     stack on the right edge so up/down/✕ stay aligned
+                     regardless of how many field rows are below. -->
+                <div class="we-controls">
+                  <button
+                    class="sw-btn ghost small"
+                    type="button"
+                    :disabled="i === 0"
+                    title="Move up"
+                    @click="moveWidget(i, -1)"
+                  >↑</button>
+                  <button
+                    class="sw-btn ghost small"
+                    type="button"
+                    :disabled="i === currentWidgets.length - 1"
+                    title="Move down"
+                    @click="moveWidget(i, 1)"
+                  >↓</button>
+                  <button
+                    class="sw-btn danger"
+                    type="button"
+                    title="Delete"
+                    @click="deleteWidget(i)"
+                  >✕</button>
+                </div>
               </div>
             </li>
           </ul>
@@ -968,17 +1019,27 @@ function toggleComponent(key: ComponentKey): void {
   gap: 10px;
   padding: 12px 16px;
 }
-.we-handle {
+.we-controls {
   display: flex;
   flex-direction: column;
   gap: 4px;
-  padding-top: 18px;
-}
-.we-handle .sw-btn {
-  width: 24px;
-  height: 22px;
+  align-self: flex-start;
+  /* Push down so the top button aligns with the FIRST input row,
+   * skipping the field labels above. Label is ~13.5px + 3px gap. */
+  padding-top: 17px;
+}
+.we-controls .sw-btn {
+  width: 28px;
+  height: 24px;
   padding: 0;
-  font-size: 10px;
+  font-size: 11px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+.we-controls .sw-btn[disabled] {
+  opacity: 0.35;
+  cursor: not-allowed;
 }
 .we-fields {
   flex: 1;
@@ -1031,15 +1092,17 @@ function toggleComponent(key: ComponentKey): void {
   border-color: var(--sw-accent-line);
 }
 .sw-btn.danger {
-  width: 26px;
-  height: 26px;
-  padding: 0;
-  font-size: 11px;
-  margin-top: 18px;
   border-color: rgba(239, 68, 68, 0.3);
   color: #f87171;
 }
 .sw-btn.danger:hover {
   background: var(--sw-err-soft);
 }
+.we-controls .sw-btn.danger {
+  /* Sized through .we-controls .sw-btn — no width/height override. */
+  margin-top: 4px;
+  border-top: 1px solid var(--sw-line);
+  padding-top: 0;
+  border-radius: 4px;
+}
 </style>

Reply via email to