This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch refactor/layer-dashboards-admin-decompose in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit dc4b1d0026f4292be9964d4fd7c3345687e37bcd Author: Wu Sheng <[email protected]> AuthorDate: Sat Jun 27 10:23:20 2026 +0800 refactor(layer-templates): extract MetricDefinitionRow across all scopes The metric-definition row, repeated 13x across the topology / instance-topology / deployment / dependency / process config editors (id/label/MQE/unit/agg plus opt-in role and a collapsible thresholds block), becomes a feature-local MetricDefinitionRow.vue. The metric is a v-model (defineModel) so the parent's reactive draft object is still edited in place; role / thresholds and the exact per-scope MQE/unit placeholders are matched to each original row; move/remove are emitted so the p [...] Parent 6543 -> 6365 lines; type-check / lint / build clean; admin renders clean. --- .../admin/layer-templates/LayerDashboardsAdmin.vue | 482 +++++++-------------- .../admin/layer-templates/MetricDefinitionRow.vue | 155 +++++++ 2 files changed, 307 insertions(+), 330 deletions(-) diff --git a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue index d4b895e..fdd6a42 100644 --- a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue +++ b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue @@ -84,6 +84,7 @@ import MqeExpressionInput from '@/features/admin/_shared/MqeExpressionInput.vue' import { useTemplateSync } from '@/features/admin/_shared/useTemplateSync'; import Modal from '@/features/operate/_shared/Modal.vue'; import MonacoDiff from '@/features/operate/_shared/MonacoDiff.vue'; +import MetricDefinitionRow from './MetricDefinitionRow.vue'; // OAP UI-template sync status for layer dashboards. Drives the banner + // Save gating + per-row badge below. @@ -1775,13 +1776,6 @@ function moveMetric(bucket: MetricBucket, i: number, dir: -1 | 1): void { if (j < 0 || j >= list.length) return; [list[i], list[j]] = [list[j], list[i]]; } -function toggleThresholds(m: TopologyMetricDef): void { - if (m.thresholds) { - m.thresholds = undefined; - } else { - m.thresholds = { ok: 0.1, warn: 1, danger: 5 }; - } -} const topologyNodeMetrics = computed(() => getMetricList('node')); const topologyServerMetrics = computed(() => getMetricList('linkServer')); @@ -3255,62 +3249,21 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="topologyNodeMetrics.length === 0" class="topo-cfg-empty">No node metrics. Click "+ Add" to start.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in topologyNodeMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"> - <span>id</span> - <input v-model="m.id" type="text" class="mf-input mono" /> - </label> - <label class="mf"> - <span>label</span> - <input v-model="m.label" type="text" class="mf-input" /> - </label> - <label class="mf mf-wide"> - <span>MQE</span> - <input v-model="m.mqe" type="text" class="mf-input mono" placeholder="service_cpm" /> - </label> - <label class="mf mf-narrow"> - <span>unit</span> - <input v-model="m.unit" type="text" class="mf-input" placeholder="rpm" /> - </label> - <label class="mf"> - <span>role</span> - <select v-model="m.role" class="mf-input"> - <option v-for="o in TOPOLOGY_ROLE_OPTIONS" :key="String(o.value)" :value="o.value || undefined">{{ o.label }}</option> - </select> - </label> - <label class="mf mf-narrow"> - <span>agg</span> - <select v-model="m.aggregation" class="mf-input"> - <option value="avg">avg</option> - <option value="sum">sum</option> - </select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" title="Move up" @click="moveMetric('node', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === topologyNodeMetrics.length - 1" title="Move down" @click="moveMetric('node', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" title="Remove" @click="removeMetric('node', i)">×</button> - </div> - </div> - <div class="metric-thresholds"> - <button class="sw-btn small ghost" type="button" @click="toggleThresholds(m)"> - {{ m.thresholds ? '− Thresholds' : '+ Thresholds' }} - </button> - <template v-if="m.thresholds"> - <label class="mf mf-narrow"><span>ok ≤</span><input v-model.number="m.thresholds.ok" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-narrow"><span>warn ≤</span><input v-model.number="m.thresholds.warn" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-narrow"><span>danger ≤</span><input v-model.number="m.thresholds.danger" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-checkbox"> - <input v-model="m.thresholds.invertHealth" type="checkbox" /> - <span>invert (higher = better)</span> - </label> - <label v-if="m.thresholds.invertHealth" class="mf mf-narrow"> - <span>base</span> - <input v-model.number="m.thresholds.invertBase" type="number" step="1" class="mf-input" placeholder="100" /> - </label> - </template> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in topologyNodeMetrics" + :key="i" + v-model:metric="topologyNodeMetrics[i]" + :role-options="TOPOLOGY_ROLE_OPTIONS" + show-role + show-thresholds + mqe-placeholder="service_cpm" + unit-placeholder="rpm" + :can-move-up="i > 0" + :can-move-down="i < topologyNodeMetrics.length - 1" + @move-up="moveMetric('node', i, -1)" + @move-down="moveMetric('node', i, 1)" + @remove="removeMetric('node', i)" + /> </div> </div> @@ -3322,25 +3275,16 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="topologyServerMetrics.length === 0" class="topo-cfg-empty">No server-side metrics.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in topologyServerMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" /></label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"> - <option value="avg">avg</option> - <option value="sum">sum</option> - </select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" @click="moveMetric('linkServer', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === topologyServerMetrics.length - 1" @click="moveMetric('linkServer', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" @click="removeMetric('linkServer', i)">×</button> - </div> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in topologyServerMetrics" + :key="i" + v-model:metric="topologyServerMetrics[i]" + :can-move-up="i > 0" + :can-move-down="i < topologyServerMetrics.length - 1" + @move-up="moveMetric('linkServer', i, -1)" + @move-down="moveMetric('linkServer', i, 1)" + @remove="removeMetric('linkServer', i)" + /> </div> </div> @@ -3352,25 +3296,16 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="topologyClientMetrics.length === 0" class="topo-cfg-empty">No client-side metrics.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in topologyClientMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" /></label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"> - <option value="avg">avg</option> - <option value="sum">sum</option> - </select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" @click="moveMetric('linkClient', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === topologyClientMetrics.length - 1" @click="moveMetric('linkClient', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" @click="removeMetric('linkClient', i)">×</button> - </div> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in topologyClientMetrics" + :key="i" + v-model:metric="topologyClientMetrics[i]" + :can-move-up="i > 0" + :can-move-down="i < topologyClientMetrics.length - 1" + @move-up="moveMetric('linkClient', i, -1)" + @move-down="moveMetric('linkClient', i, 1)" + @remove="removeMetric('linkClient', i)" + /> </div> </div> @@ -3398,40 +3333,21 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="instanceNodeMetrics.length === 0" class="topo-cfg-empty">No node metrics. Click "+ Add" to start.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in instanceNodeMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" placeholder="service_instance_cpm" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" placeholder="rpm" /></label> - <label class="mf"><span>role</span> - <select v-model="m.role" class="mf-input"> - <option v-for="o in TOPOLOGY_ROLE_OPTIONS" :key="String(o.value)" :value="o.value || undefined">{{ o.label }}</option> - </select> - </label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"> - <option value="avg">avg</option> - <option value="sum">sum</option> - </select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" title="Move up" @click="moveMetric('instNode', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === instanceNodeMetrics.length - 1" title="Move down" @click="moveMetric('instNode', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" title="Remove" @click="removeMetric('instNode', i)">×</button> - </div> - </div> - <div class="metric-thresholds"> - <button class="sw-btn small ghost" type="button" @click="toggleThresholds(m)">{{ m.thresholds ? '− Thresholds' : '+ Thresholds' }}</button> - <template v-if="m.thresholds"> - <label class="mf mf-narrow"><span>ok ≤</span><input v-model.number="m.thresholds.ok" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-narrow"><span>warn ≤</span><input v-model.number="m.thresholds.warn" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-narrow"><span>danger ≤</span><input v-model.number="m.thresholds.danger" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-checkbox"><input v-model="m.thresholds.invertHealth" type="checkbox" /><span>invert (higher = better)</span></label> - <label v-if="m.thresholds.invertHealth" class="mf mf-narrow"><span>base</span><input v-model.number="m.thresholds.invertBase" type="number" step="1" class="mf-input" placeholder="100" /></label> - </template> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in instanceNodeMetrics" + :key="i" + v-model:metric="instanceNodeMetrics[i]" + :role-options="TOPOLOGY_ROLE_OPTIONS" + show-role + show-thresholds + mqe-placeholder="service_instance_cpm" + unit-placeholder="rpm" + :can-move-up="i > 0" + :can-move-down="i < instanceNodeMetrics.length - 1" + @move-up="moveMetric('instNode', i, -1)" + @move-down="moveMetric('instNode', i, 1)" + @remove="removeMetric('instNode', i)" + /> </div> </div> @@ -3443,22 +3359,16 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="instanceServerMetrics.length === 0" class="topo-cfg-empty">No server-side metrics.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in instanceServerMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" /></label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"><option value="avg">avg</option><option value="sum">sum</option></select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" @click="moveMetric('instLinkServer', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === instanceServerMetrics.length - 1" @click="moveMetric('instLinkServer', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" @click="removeMetric('instLinkServer', i)">×</button> - </div> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in instanceServerMetrics" + :key="i" + v-model:metric="instanceServerMetrics[i]" + :can-move-up="i > 0" + :can-move-down="i < instanceServerMetrics.length - 1" + @move-up="moveMetric('instLinkServer', i, -1)" + @move-down="moveMetric('instLinkServer', i, 1)" + @remove="removeMetric('instLinkServer', i)" + /> </div> </div> @@ -3470,22 +3380,16 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="instanceClientMetrics.length === 0" class="topo-cfg-empty">No client-side metrics.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in instanceClientMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" /></label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"><option value="avg">avg</option><option value="sum">sum</option></select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" @click="moveMetric('instLinkClient', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === instanceClientMetrics.length - 1" @click="moveMetric('instLinkClient', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" @click="removeMetric('instLinkClient', i)">×</button> - </div> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in instanceClientMetrics" + :key="i" + v-model:metric="instanceClientMetrics[i]" + :can-move-up="i > 0" + :can-move-down="i < instanceClientMetrics.length - 1" + @move-up="moveMetric('instLinkClient', i, -1)" + @move-down="moveMetric('instLinkClient', i, 1)" + @remove="removeMetric('instLinkClient', i)" + /> </div> </div> </template> @@ -3737,37 +3641,21 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="deploymentNodeMetrics.length === 0" class="topo-cfg-empty">No node metrics. Click "+ Add" to start.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in deploymentNodeMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" placeholder="service_instance_cpm" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" placeholder="rpm" /></label> - <label class="mf"><span>role</span> - <select v-model="m.role" class="mf-input"> - <option v-for="o in TOPOLOGY_ROLE_OPTIONS" :key="String(o.value)" :value="o.value || undefined">{{ o.label }}</option> - </select> - </label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"><option value="avg">avg</option><option value="sum">sum</option></select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" title="Move up" @click="moveMetric('sitNode', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === deploymentNodeMetrics.length - 1" title="Move down" @click="moveMetric('sitNode', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" title="Remove" @click="removeMetric('sitNode', i)">×</button> - </div> - </div> - <div class="metric-thresholds"> - <button class="sw-btn small ghost" type="button" @click="toggleThresholds(m)">{{ m.thresholds ? '− Thresholds' : '+ Thresholds' }}</button> - <template v-if="m.thresholds"> - <label class="mf mf-narrow"><span>ok ≤</span><input v-model.number="m.thresholds.ok" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-narrow"><span>warn ≤</span><input v-model.number="m.thresholds.warn" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-narrow"><span>danger ≤</span><input v-model.number="m.thresholds.danger" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-checkbox"><input v-model="m.thresholds.invertHealth" type="checkbox" /><span>invert (higher = better)</span></label> - <label v-if="m.thresholds.invertHealth" class="mf mf-narrow"><span>base</span><input v-model.number="m.thresholds.invertBase" type="number" step="1" class="mf-input" placeholder="100" /></label> - </template> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in deploymentNodeMetrics" + :key="i" + v-model:metric="deploymentNodeMetrics[i]" + :role-options="TOPOLOGY_ROLE_OPTIONS" + show-role + show-thresholds + mqe-placeholder="service_instance_cpm" + unit-placeholder="rpm" + :can-move-up="i > 0" + :can-move-down="i < deploymentNodeMetrics.length - 1" + @move-up="moveMetric('sitNode', i, -1)" + @move-down="moveMetric('sitNode', i, 1)" + @remove="removeMetric('sitNode', i)" + /> </div> </div> @@ -3779,22 +3667,16 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="deploymentServerMetrics.length === 0" class="topo-cfg-empty">No server-side metrics.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in deploymentServerMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" /></label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"><option value="avg">avg</option><option value="sum">sum</option></select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" @click="moveMetric('sitLinkServer', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === deploymentServerMetrics.length - 1" @click="moveMetric('sitLinkServer', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" @click="removeMetric('sitLinkServer', i)">×</button> - </div> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in deploymentServerMetrics" + :key="i" + v-model:metric="deploymentServerMetrics[i]" + :can-move-up="i > 0" + :can-move-down="i < deploymentServerMetrics.length - 1" + @move-up="moveMetric('sitLinkServer', i, -1)" + @move-down="moveMetric('sitLinkServer', i, 1)" + @remove="removeMetric('sitLinkServer', i)" + /> </div> </div> @@ -3806,22 +3688,16 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="deploymentClientMetrics.length === 0" class="topo-cfg-empty">No client-side metrics.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in deploymentClientMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" /></label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"><option value="avg">avg</option><option value="sum">sum</option></select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" @click="moveMetric('sitLinkClient', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === deploymentClientMetrics.length - 1" @click="moveMetric('sitLinkClient', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" @click="removeMetric('sitLinkClient', i)">×</button> - </div> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in deploymentClientMetrics" + :key="i" + v-model:metric="deploymentClientMetrics[i]" + :can-move-up="i > 0" + :can-move-down="i < deploymentClientMetrics.length - 1" + @move-up="moveMetric('sitLinkClient', i, -1)" + @move-down="moveMetric('sitLinkClient', i, 1)" + @remove="removeMetric('sitLinkClient', i)" + /> </div> </div> </div> @@ -3855,49 +3731,20 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="epDepNodeMetrics.length === 0" class="topo-cfg-empty">No node metrics.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in epDepNodeMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" placeholder="endpoint_cpm" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" /></label> - <label class="mf"> - <span>role</span> - <select v-model="m.role" class="mf-input"> - <option v-for="o in TOPOLOGY_ROLE_OPTIONS" :key="String(o.value)" :value="o.value || undefined">{{ o.label }}</option> - </select> - </label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"> - <option value="avg">avg</option> - <option value="sum">sum</option> - </select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" @click="moveMetric('node', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === epDepNodeMetrics.length - 1" @click="moveMetric('node', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" @click="removeMetric('node', i)">×</button> - </div> - </div> - <div class="metric-thresholds"> - <button class="sw-btn small ghost" type="button" @click="toggleThresholds(m)"> - {{ m.thresholds ? '− Thresholds' : '+ Thresholds' }} - </button> - <template v-if="m.thresholds"> - <label class="mf mf-narrow"><span>ok ≤</span><input v-model.number="m.thresholds.ok" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-narrow"><span>warn ≤</span><input v-model.number="m.thresholds.warn" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-narrow"><span>danger ≤</span><input v-model.number="m.thresholds.danger" type="number" step="0.1" class="mf-input" /></label> - <label class="mf mf-checkbox"> - <input v-model="m.thresholds.invertHealth" type="checkbox" /> - <span>invert (higher = better)</span> - </label> - <label v-if="m.thresholds.invertHealth" class="mf mf-narrow"> - <span>base</span> - <input v-model.number="m.thresholds.invertBase" type="number" step="1" class="mf-input" placeholder="100" /> - </label> - </template> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in epDepNodeMetrics" + :key="i" + v-model:metric="epDepNodeMetrics[i]" + :role-options="TOPOLOGY_ROLE_OPTIONS" + show-role + show-thresholds + mqe-placeholder="endpoint_cpm" + :can-move-up="i > 0" + :can-move-down="i < epDepNodeMetrics.length - 1" + @move-up="moveMetric('node', i, -1)" + @move-down="moveMetric('node', i, 1)" + @remove="removeMetric('node', i)" + /> </div> </div> @@ -3909,25 +3756,16 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="epDepLinkMetrics.length === 0" class="topo-cfg-empty">No link metrics.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in epDepLinkMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" /></label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"> - <option value="avg">avg</option> - <option value="sum">sum</option> - </select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" @click="moveMetric('link', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === epDepLinkMetrics.length - 1" @click="moveMetric('link', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" @click="removeMetric('link', i)">×</button> - </div> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in epDepLinkMetrics" + :key="i" + v-model:metric="epDepLinkMetrics[i]" + :can-move-up="i > 0" + :can-move-down="i < epDepLinkMetrics.length - 1" + @move-up="moveMetric('link', i, -1)" + @move-down="moveMetric('link', i, 1)" + @remove="removeMetric('link', i)" + /> </div> </div> </div> @@ -3950,25 +3788,17 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="processEdgeClientMetrics.length === 0" class="topo-cfg-empty">No client-side metrics.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in processEdgeClientMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" placeholder="process_relation_client_write_cpm" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" /></label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"> - <option value="avg">avg</option> - <option value="sum">sum</option> - </select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" @click="moveMetric('edgeClient', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === processEdgeClientMetrics.length - 1" @click="moveMetric('edgeClient', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" @click="removeMetric('edgeClient', i)">×</button> - </div> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in processEdgeClientMetrics" + :key="i" + v-model:metric="processEdgeClientMetrics[i]" + mqe-placeholder="process_relation_client_write_cpm" + :can-move-up="i > 0" + :can-move-down="i < processEdgeClientMetrics.length - 1" + @move-up="moveMetric('edgeClient', i, -1)" + @move-down="moveMetric('edgeClient', i, 1)" + @remove="removeMetric('edgeClient', i)" + /> </div> </div> @@ -3980,25 +3810,17 @@ const namingTest = computed<NamingTestResult>(() => { </header> <div v-if="processEdgeServerMetrics.length === 0" class="topo-cfg-empty">No server-side metrics.</div> <div v-else class="metric-list"> - <article v-for="(m, i) in processEdgeServerMetrics" :key="i" class="metric-row"> - <div class="metric-row-head"> - <label class="mf"><span>id</span><input v-model="m.id" type="text" class="mf-input mono" /></label> - <label class="mf"><span>label</span><input v-model="m.label" type="text" class="mf-input" /></label> - <label class="mf mf-wide"><span>MQE</span><input v-model="m.mqe" type="text" class="mf-input mono" placeholder="process_relation_server_write_cpm" /></label> - <label class="mf mf-narrow"><span>unit</span><input v-model="m.unit" type="text" class="mf-input" /></label> - <label class="mf mf-narrow"><span>agg</span> - <select v-model="m.aggregation" class="mf-input"> - <option value="avg">avg</option> - <option value="sum">sum</option> - </select> - </label> - <div class="metric-row-actions"> - <button class="sw-btn small ghost" type="button" :disabled="i === 0" @click="moveMetric('edgeServer', i, -1)">↑</button> - <button class="sw-btn small ghost" type="button" :disabled="i === processEdgeServerMetrics.length - 1" @click="moveMetric('edgeServer', i, 1)">↓</button> - <button class="sw-btn small ghost danger" type="button" @click="removeMetric('edgeServer', i)">×</button> - </div> - </div> - </article> + <MetricDefinitionRow + v-for="(_m, i) in processEdgeServerMetrics" + :key="i" + v-model:metric="processEdgeServerMetrics[i]" + mqe-placeholder="process_relation_server_write_cpm" + :can-move-up="i > 0" + :can-move-down="i < processEdgeServerMetrics.length - 1" + @move-up="moveMetric('edgeServer', i, -1)" + @move-down="moveMetric('edgeServer', i, 1)" + @remove="removeMetric('edgeServer', i)" + /> </div> </div> </div> diff --git a/apps/ui/src/features/admin/layer-templates/MetricDefinitionRow.vue b/apps/ui/src/features/admin/layer-templates/MetricDefinitionRow.vue new file mode 100644 index 0000000..1bf0098 --- /dev/null +++ b/apps/ui/src/features/admin/layer-templates/MetricDefinitionRow.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. +--> +<!-- + One editable topology/deployment metric definition (id / label / MQE / unit / + agg, optional role + thresholds). Reused across every scope's metric list in + LayerDashboardsAdmin — `role` and the collapsible `thresholds` block are + opt-in per bucket (node rows show them, link rows don't). The metric object is + mutated in place (it's part of the live draft); move/remove are emitted so the + parent wires the bucket-specific handler. +--> +<script setup lang="ts"> +import type { TopologyMetricDef } from '@/api/client'; + +const metric = defineModel<TopologyMetricDef>('metric', { required: true }); +defineProps<{ + roleOptions?: ReadonlyArray<{ value: TopologyMetricDef['role'] | ''; label: string }>; + showRole?: boolean; + showThresholds?: boolean; + mqePlaceholder?: string; + unitPlaceholder?: string; + canMoveUp: boolean; + canMoveDown: boolean; +}>(); +defineEmits<{ moveUp: []; moveDown: []; remove: [] }>(); + +function toggleThresholds(m: TopologyMetricDef): void { + m.thresholds = m.thresholds ? undefined : { ok: 0.1, warn: 1, danger: 5 }; +} +</script> + +<template> + <article class="metric-row"> + <div class="metric-row-head"> + <label class="mf"><span>id</span><input v-model="metric.id" type="text" class="mf-input mono" /></label> + <label class="mf"><span>label</span><input v-model="metric.label" type="text" class="mf-input" /></label> + <label class="mf mf-wide"><span>MQE</span><input v-model="metric.mqe" type="text" class="mf-input mono" :placeholder="mqePlaceholder" /></label> + <label class="mf mf-narrow"><span>unit</span><input v-model="metric.unit" type="text" class="mf-input" :placeholder="unitPlaceholder" /></label> + <label v-if="showRole" class="mf"> + <span>role</span> + <select v-model="metric.role" class="mf-input"> + <option v-for="o in roleOptions" :key="String(o.value)" :value="o.value || undefined">{{ o.label }}</option> + </select> + </label> + <label class="mf mf-narrow"> + <span>agg</span> + <select v-model="metric.aggregation" class="mf-input"> + <option value="avg">avg</option> + <option value="sum">sum</option> + </select> + </label> + <div class="metric-row-actions"> + <button class="sw-btn small ghost" type="button" :disabled="!canMoveUp" title="Move up" @click="$emit('moveUp')">↑</button> + <button class="sw-btn small ghost" type="button" :disabled="!canMoveDown" title="Move down" @click="$emit('moveDown')">↓</button> + <button class="sw-btn small ghost danger" type="button" title="Remove" @click="$emit('remove')">×</button> + </div> + </div> + <div v-if="showThresholds" class="metric-thresholds"> + <button class="sw-btn small ghost" type="button" @click="toggleThresholds(metric)"> + {{ metric.thresholds ? '− Thresholds' : '+ Thresholds' }} + </button> + <template v-if="metric.thresholds"> + <label class="mf mf-narrow"><span>ok ≤</span><input v-model.number="metric.thresholds.ok" type="number" step="0.1" class="mf-input" /></label> + <label class="mf mf-narrow"><span>warn ≤</span><input v-model.number="metric.thresholds.warn" type="number" step="0.1" class="mf-input" /></label> + <label class="mf mf-narrow"><span>danger ≤</span><input v-model.number="metric.thresholds.danger" type="number" step="0.1" class="mf-input" /></label> + <label class="mf mf-checkbox"> + <input v-model="metric.thresholds.invertHealth" type="checkbox" /> + <span>invert (higher = better)</span> + </label> + <label v-if="metric.thresholds.invertHealth" class="mf mf-narrow"> + <span>base</span> + <input v-model.number="metric.thresholds.invertBase" type="number" step="1" class="mf-input" placeholder="100" /> + </label> + </template> + </div> + </article> +</template> + +<style scoped> +.metric-row { + background: var(--sw-bg-1); + border: 1px solid var(--sw-line); + border-radius: 4px; + padding: 8px 10px; +} +.metric-row-head { + display: flex; + align-items: flex-end; + gap: 8px; + flex-wrap: wrap; +} +.metric-row-actions { + display: inline-flex; + gap: 4px; + margin-left: auto; + align-self: flex-end; +} +.metric-thresholds { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: flex-end; + margin-top: 6px; + padding-top: 6px; + border-top: 1px dashed var(--sw-line); +} +.mf { + display: inline-flex; + flex-direction: column; + gap: 3px; + font-size: 10px; + color: var(--sw-fg-3); + letter-spacing: 0.04em; + text-transform: uppercase; + min-width: 110px; +} +.mf.mf-wide { min-width: 220px; flex: 1; } +.mf.mf-narrow { min-width: 80px; } +.mf.mf-checkbox { + flex-direction: row; + align-items: center; + text-transform: none; + letter-spacing: 0; + font-size: 10.5px; + color: var(--sw-fg-2); + min-width: auto; +} +.mf.mf-checkbox input { accent-color: var(--sw-accent); } +.mf-input { + height: 26px; + padding: 0 6px; + background: var(--sw-bg-2); + border: 1px solid var(--sw-line-2); + border-radius: 4px; + color: var(--sw-fg-0); + font: inherit; + font-size: 11px; + width: 100%; + box-sizing: border-box; +} +.mf-input.mono { font-family: var(--sw-mono); } +</style>
