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
The following commit(s) were added to refs/heads/main by this push:
new 6be5e33 config: aliases supersedes slots in JSON; admin gains metrics
editor
6be5e33 is described below
commit 6be5e332ee131ecdace24bcb96d0977a6678f26a
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 12 22:18:25 2026 +0800
config: aliases supersedes slots in JSON; admin gains metrics editor
JSON schema rename: per-layer entity-term aliases live under
'aliases' in the JSON now (plural, distinct from the existing
top-level 'alias' which is the layer display name). Operators write
'aliases' going forward; the loader still accepts legacy 'slots'
files and normalizes to the internal 'slots' field, so existing
templates keep working unchanged. general.json migrated.
Admin / Layer dashboards page gains a Metrics editor section above
the scope tabs:
- Per-column inline editing: metric / label / unit / aggregation /
MQE override / scale / precision. Add column + delete row.
- Top-of-block dropdowns for orderBy / throughput / spark — each
selects from the defined columns.
- Mutations flow through the existing draft → save path, so saving
rewrites the JSON template with the new metric columns and the
BFF reloads its cache.
AdminLayerTemplate type gains scale + precision on column entries to
match the BFF zod schema. No wire-protocol break — the BFF already
accepted these fields in admin saves; the UI just couldn't bind to
them before.
---
apps/bff/src/layers/config/general.json | 2 +-
apps/bff/src/layers/loader.ts | 14 +-
apps/ui/src/api/client.ts | 2 +
apps/ui/src/views/admin/LayerDashboardsAdmin.vue | 188 +++++++++++++++++++++++
4 files changed, 203 insertions(+), 3 deletions(-)
diff --git a/apps/bff/src/layers/config/general.json
b/apps/bff/src/layers/config/general.json
index ff00563..07911c3 100644
--- a/apps/bff/src/layers/config/general.json
+++ b/apps/bff/src/layers/config/general.json
@@ -3,7 +3,7 @@
"alias": "General Service",
"color": "var(--sw-accent)",
"documentLink":
"https://skywalking.apache.org/docs/main/latest/en/setup/service-agent/",
- "slots": {
+ "aliases": {
"services": "Services",
"instances": "Instances",
"endpoints": "API",
diff --git a/apps/bff/src/layers/loader.ts b/apps/bff/src/layers/loader.ts
index e0a1b4f..bf69aa0 100644
--- a/apps/bff/src/layers/loader.ts
+++ b/apps/bff/src/layers/loader.ts
@@ -118,9 +118,9 @@ function load(): Map<string, LayerTemplate> {
for (const file of readdirSync(CONFIG_DIR)) {
if (!file.endsWith('.json')) continue;
const raw = readFileSync(join(CONFIG_DIR, file), 'utf-8');
- let parsed: LayerTemplate;
+ let parsed: LayerTemplate & { alias_terms?: LayerSlotsConfig; alias?:
LayerSlotsConfig | string };
try {
- parsed = JSON.parse(raw) as LayerTemplate;
+ parsed = JSON.parse(raw);
} catch (err) {
throw new Error(`failed to parse layer config ${file}: ${err instanceof
Error ? err.message : err}`);
}
@@ -130,6 +130,16 @@ function load(): Map<string, LayerTemplate> {
`layer config ${file}: file basename does not match \`key\`
(${parsed.key})`,
);
}
+ // Schema migration: per-layer entity term overrides used to live
+ // under `slots` in the JSON; the more readable `aliases` (plural,
+ // distinct from the existing top-level `alias` = layer display
+ // name) is now accepted as the primary key. TS code keeps `slots`
+ // internally — we normalize here so the rest of the BFF + UI
+ // doesn't need to know which the operator wrote.
+ const aliases = (parsed as { aliases?: LayerSlotsConfig }).aliases;
+ if (!parsed.slots && aliases) {
+ parsed.slots = aliases;
+ }
// Migrate legacy `widgets` (flat array) → `dashboards.service` so
// the rest of the codebase only needs to know about the new shape.
if (parsed.widgets && (!parsed.dashboards || !parsed.dashboards.service)) {
diff --git a/apps/ui/src/api/client.ts b/apps/ui/src/api/client.ts
index a13827b..932acca 100644
--- a/apps/ui/src/api/client.ts
+++ b/apps/ui/src/api/client.ts
@@ -80,6 +80,8 @@ export interface AdminLayerTemplate {
unit?: string;
mqe?: string;
aggregation?: 'sum' | 'avg';
+ scale?: number;
+ precision?: number;
}>;
};
widgets: DashboardWidget[];
diff --git a/apps/ui/src/views/admin/LayerDashboardsAdmin.vue
b/apps/ui/src/views/admin/LayerDashboardsAdmin.vue
index 16c40db..fb30e9d 100644
--- a/apps/ui/src/views/admin/LayerDashboardsAdmin.vue
+++ b/apps/ui/src/views/admin/LayerDashboardsAdmin.vue
@@ -168,6 +168,47 @@ function reset(): void {
const selectedTpl = computed(() => draft.template);
const currentWidgets = computed(() => widgetsFor(activeScope.value));
+/**
+ * Metrics-block editor. The metrics block lives directly on the
+ * draft template; mutations flow through Vue's reactive proxy so the
+ * dirty diff picks them up and Save enables. We ensure the
+ * `metrics.columns` array exists before binding so the template can
+ * safely v-model into it.
+ */
+function ensureMetrics(): NonNullable<AdminLayerTemplate['metrics']> {
+ if (!draft.template) throw new Error('no template selected');
+ if (!draft.template.metrics) {
+ (draft.template as AdminLayerTemplate).metrics = {};
+ }
+ return draft.template.metrics as NonNullable<AdminLayerTemplate['metrics']>;
+}
+const metricsModel = computed(() => {
+ if (!draft.template) return null;
+ // Touch ensureMetrics on read so the keys are present for v-model.
+ ensureMetrics();
+ return draft.template.metrics as NonNullable<AdminLayerTemplate['metrics']>;
+});
+const metricsColumns = computed(() => {
+ if (!draft.template) return [];
+ const m = ensureMetrics();
+ if (!m.columns) m.columns = [];
+ return m.columns;
+});
+function addMetricColumn(): void {
+ if (!draft.template) return;
+ const m = ensureMetrics();
+ if (!m.columns) m.columns = [];
+ m.columns.push({
+ metric: `metric_${m.columns.length + 1}`,
+ label: `Metric ${m.columns.length + 1}`,
+ aggregation: 'avg',
+ });
+}
+function deleteMetricColumn(i: number): void {
+ if (!draft.template?.metrics?.columns) return;
+ draft.template.metrics.columns.splice(i, 1);
+}
+
function componentFlags(t: AdminLayerTemplate): string[] {
const c = t.components;
const out: string[] = [];
@@ -253,6 +294,76 @@ function componentFlags(t: AdminLayerTemplate): string[] {
</div>
</section>
+ <!-- Metrics editor (the layer's summary KPI columns + the
+ orderBy / throughput / spark selectors). These drive the
+ Overview KPI tile and the per-layer header summary. -->
+ <section class="sw-card metrics-card">
+ <div class="card-head">
+ <h4>Summary metrics</h4>
+ <span class="sub">columns shown on the Overview KPI tile +
per-layer header</span>
+ <button class="sw-btn add" type="button"
@click="addMetricColumn">+ Add column</button>
+ </div>
+ <div v-if="metricsModel" class="metrics-keys">
+ <label>
+ <span>orderBy</span>
+ <select v-model="metricsModel.orderBy">
+ <option :value="undefined">(first column)</option>
+ <option v-for="c in metricsColumns" :key="c.metric"
:value="c.metric">{{ c.metric }}</option>
+ </select>
+ </label>
+ <label>
+ <span>throughput</span>
+ <select v-model="metricsModel.throughput">
+ <option :value="undefined">(orderBy)</option>
+ <option v-for="c in metricsColumns" :key="c.metric"
:value="c.metric">{{ c.metric }}</option>
+ </select>
+ </label>
+ <label>
+ <span>spark</span>
+ <select v-model="metricsModel.spark">
+ <option :value="undefined">(throughput)</option>
+ <option v-for="c in metricsColumns" :key="c.metric"
:value="c.metric">{{ c.metric }}</option>
+ </select>
+ </label>
+ </div>
+ <div v-if="metricsColumns.length === 0" class="empty inset">
+ No metric columns defined. Click "Add column" to start.
+ </div>
+ <table v-else class="sw-table metrics-table">
+ <thead>
+ <tr>
+ <th>metric</th>
+ <th>label</th>
+ <th>unit</th>
+ <th>aggregation</th>
+ <th class="grow">mqe</th>
+ <th>scale</th>
+ <th>precision</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(c, i) in metricsColumns" :key="i">
+ <td><input class="mono" v-model="c.metric" /></td>
+ <td><input v-model="c.label" /></td>
+ <td><input v-model="c.unit" placeholder="—" /></td>
+ <td>
+ <select v-model="c.aggregation">
+ <option value="sum">sum</option>
+ <option value="avg">avg</option>
+ </select>
+ </td>
+ <td><input class="mono" v-model="c.mqe" placeholder="catalog
default" /></td>
+ <td><input type="number" step="any" v-model.number="c.scale"
placeholder="1" /></td>
+ <td><input type="number" min="0" max="6"
v-model.number="c.precision" placeholder="auto" /></td>
+ <td>
+ <button class="sw-btn danger" type="button"
@click="deleteMetricColumn(i)">✕</button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+
<!-- Scope tabs -->
<nav class="scope-tabs sw-card">
<button
@@ -568,6 +679,83 @@ function componentFlags(t: AdminLayerTemplate): string[] {
}
.scope-tab.on .count { color: var(--sw-accent-2); }
+.metrics-card { padding: 0; }
+.metrics-card .card-head .add {
+ margin-left: auto;
+ font-size: 11.5px;
+ background: var(--sw-accent-soft);
+ color: var(--sw-accent-2);
+ border-color: var(--sw-accent-line);
+}
+.metrics-keys {
+ display: flex;
+ gap: 14px;
+ padding: 10px 16px;
+ border-bottom: 1px dashed var(--sw-line);
+}
+.metrics-keys label {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ font-size: 10.5px;
+ color: var(--sw-fg-3);
+ min-width: 120px;
+}
+.metrics-keys select {
+ height: 26px;
+ padding: 0 8px;
+ background: var(--sw-bg-2);
+ border: 1px solid var(--sw-line-2);
+ border-radius: 4px;
+ color: var(--sw-fg-0);
+ font: inherit;
+ font-size: 11.5px;
+}
+.metrics-table {
+ width: 100%;
+}
+.metrics-table th {
+ text-align: left;
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--sw-fg-3);
+ font-weight: 500;
+ padding: 8px 10px 6px;
+ border-bottom: 1px solid var(--sw-line);
+}
+.metrics-table th.grow {
+ width: 35%;
+}
+.metrics-table td {
+ padding: 6px 10px;
+ border-bottom: 1px solid var(--sw-line);
+}
+.metrics-table input,
+.metrics-table select {
+ width: 100%;
+ height: 26px;
+ padding: 0 6px;
+ background: var(--sw-bg-2);
+ border: 1px solid var(--sw-line-2);
+ border-radius: 3px;
+ color: var(--sw-fg-0);
+ font: inherit;
+ font-size: 11.5px;
+}
+.metrics-table input.mono {
+ font-family: var(--sw-mono);
+ font-size: 11px;
+}
+.metrics-table .sw-btn.danger {
+ width: 26px;
+ height: 26px;
+ padding: 0;
+ font-size: 11px;
+ border-color: rgba(239, 68, 68, 0.3);
+ color: #f87171;
+}
+
.widgets-card { padding: 0; }
.card-head {
display: flex;