Skip to content

Commit 99144e0

Browse files
committed
fix(health-details): fix detail key collision, ?? for source selection, extract safeDetailId
- details v-for :key now includes loop index to handle duplicate detail names - health.details || health.components replaced with ?? and validated as plain object, eliminating false fallthrough on falsy health.details values and both sources being shared via a single healthEntries computed - Inline detail.name.replace() repeated three times replaced with safeDetailId(name, idx) helper that also falls back to 'detail_N' when all characters are special
1 parent c5700bf commit 99144e0

1 file changed

Lines changed: 26 additions & 18 deletions

File tree

spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.vue

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@
5555
:aria-labelledby="`health-${id}__${safeNameId}`"
5656
>
5757
<dl v-if="!isCollapsed" class="grid grid-cols-6 mt-2">
58-
<template v-for="detail in details" :key="detail.name">
58+
<template v-for="(detail, idx) in details" :key="`${detail.name}_${idx}`">
5959
<dt
60-
:id="`health-detail-${id}__${detail.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`"
60+
:id="`health-detail-${id}__${safeDetailId(detail.name, idx)}`"
6161
class="font-medium col-span-2"
6262
v-text="detail.name"
6363
/>
@@ -69,15 +69,15 @@
6969
class="col-span-4"
7070
role="definition"
7171
:aria-label="detail.name"
72-
:aria-labelledby="`health-detail-${id}__${detail.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`"
72+
:aria-labelledby="`health-detail-${id}__${safeDetailId(detail.name, idx)}`"
7373
v-text="prettyBytes(detail.value as number)"
7474
/>
7575
<dd
7676
v-else-if="detail.value !== null && typeof detail.value === 'object'"
7777
class="col-span-4"
7878
role="definition"
7979
:aria-label="detail.name"
80-
:aria-labelledby="`health-detail-${id}__${detail.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`"
80+
:aria-labelledby="`health-detail-${id}__${safeDetailId(detail.name, idx)}`"
8181
>
8282
<sba-formatted-obj
8383
class="overflow-auto whitespace-pre!"
@@ -88,7 +88,7 @@
8888
v-else
8989
role="definition"
9090
:aria-label="detail.name"
91-
:aria-labelledby="`health-detail-${id}__${detail.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`"
91+
:aria-labelledby="`health-detail-${id}__${safeDetailId(detail.name, idx)}`"
9292
class="wrap-break-word whitespace-pre-wrap col-span-4"
9393
v-html="autolink(String(detail.value ?? ''))"
9494
/>
@@ -134,6 +134,12 @@ const { health, name, instance, index = 0 } = defineProps<{
134134
// Sanitised name safe for use in HTML id attributes
135135
const safeNameId = computed(() => (name ?? '').replace(/[^a-zA-Z0-9_-]/g, '_'));
136136
137+
// Sanitise a detail key for use in HTML ids; fall back to its index when result would be empty
138+
function safeDetailId(detailName: string, idx: number): string {
139+
const safe = detailName.replace(/[^a-zA-Z0-9_-]/g, '_');
140+
return safe.length > 0 ? safe : `detail_${idx}`;
141+
}
142+
137143
const COLLAPSED_KEY = computed(
138144
() =>
139145
`de.codecentric.spring-boot-admin.health-details.${encodeURIComponent(name ?? '')}.${encodeURIComponent(instance?.id ?? '')}.collapsed`,
@@ -161,23 +167,25 @@ const isChildHealth = (value: any) => {
161167
return value !== null && typeof value === 'object' && 'status' in value;
162168
};
163169
164-
const details = computed(() => {
165-
if (health.details || health.components) {
166-
return Object.entries(health.details || health.components)
167-
.filter(([, value]) => !isChildHealth(value))
168-
.map(([name, value]) => ({ name, value }) as Details);
170+
const healthEntries = computed(() => {
171+
const source = health.details ?? health.components;
172+
if (source && typeof source === 'object' && !Array.isArray(source)) {
173+
return Object.entries(source);
169174
}
170175
return [];
171176
});
172177
173-
const childHealth = computed(() => {
174-
if (health.details || health.components) {
175-
return Object.entries(health.details || health.components)
176-
.filter(([, value]) => isChildHealth(value))
177-
.map(([name, value]) => ({ name, value }));
178-
}
179-
return [];
180-
});
178+
const details = computed(() =>
179+
healthEntries.value
180+
.filter(([, value]) => !isChildHealth(value))
181+
.map(([name, value]) => ({ name, value }) as Details),
182+
);
183+
184+
const childHealth = computed(() =>
185+
healthEntries.value
186+
.filter(([, value]) => isChildHealth(value))
187+
.map(([name, value]) => ({ name, value })),
188+
);
181189
182190
watch(COLLAPSED_KEY, () => {
183191
isCollapsed.value = readCollapsedFromStorage();

0 commit comments

Comments
 (0)