|
55 | 55 | :aria-labelledby="`health-${id}__${safeNameId}`" |
56 | 56 | > |
57 | 57 | <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}`"> |
59 | 59 | <dt |
60 | | - :id="`health-detail-${id}__${detail.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`" |
| 60 | + :id="`health-detail-${id}__${safeDetailId(detail.name, idx)}`" |
61 | 61 | class="font-medium col-span-2" |
62 | 62 | v-text="detail.name" |
63 | 63 | /> |
|
69 | 69 | class="col-span-4" |
70 | 70 | role="definition" |
71 | 71 | :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)}`" |
73 | 73 | v-text="prettyBytes(detail.value as number)" |
74 | 74 | /> |
75 | 75 | <dd |
76 | 76 | v-else-if="detail.value !== null && typeof detail.value === 'object'" |
77 | 77 | class="col-span-4" |
78 | 78 | role="definition" |
79 | 79 | :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)}`" |
81 | 81 | > |
82 | 82 | <sba-formatted-obj |
83 | 83 | class="overflow-auto whitespace-pre!" |
|
88 | 88 | v-else |
89 | 89 | role="definition" |
90 | 90 | :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)}`" |
92 | 92 | class="wrap-break-word whitespace-pre-wrap col-span-4" |
93 | 93 | v-html="autolink(String(detail.value ?? ''))" |
94 | 94 | /> |
@@ -134,6 +134,12 @@ const { health, name, instance, index = 0 } = defineProps<{ |
134 | 134 | // Sanitised name safe for use in HTML id attributes |
135 | 135 | const safeNameId = computed(() => (name ?? '').replace(/[^a-zA-Z0-9_-]/g, '_')); |
136 | 136 |
|
| 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 | +
|
137 | 143 | const COLLAPSED_KEY = computed( |
138 | 144 | () => |
139 | 145 | `de.codecentric.spring-boot-admin.health-details.${encodeURIComponent(name ?? '')}.${encodeURIComponent(instance?.id ?? '')}.collapsed`, |
@@ -161,23 +167,25 @@ const isChildHealth = (value: any) => { |
161 | 167 | return value !== null && typeof value === 'object' && 'status' in value; |
162 | 168 | }; |
163 | 169 |
|
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); |
169 | 174 | } |
170 | 175 | return []; |
171 | 176 | }); |
172 | 177 |
|
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 | +); |
181 | 189 |
|
182 | 190 | watch(COLLAPSED_KEY, () => { |
183 | 191 | isCollapsed.value = readCollapsedFromStorage(); |
|
0 commit comments