Skip to content

Commit 62dcf54

Browse files
committed
fix: enhance details page information fetching and subscription management
1 parent fed5a1b commit 62dcf54

9 files changed

Lines changed: 201 additions & 164 deletions

File tree

spring-boot-admin-server-ui/src/main/frontend/mixins/subscribing.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
import { Subject } from 'rxjs';
1817

1918
export default {

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

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -140,36 +140,36 @@ export default {
140140
},
141141
},
142142
methods: {
143-
initCacheMetrics() {
144-
const updateKey =
145-
this.instance.version ??
146-
this.instance.statusTimestamp ??
147-
this.instance.id;
148-
const firstInit = this.currentInstanceId === null;
149-
if (
150-
this.instance.id !== this.currentInstanceId ||
151-
updateKey !== this.currentInstanceUpdateKey
152-
) {
153-
this.currentInstanceId = this.instance.id;
154-
this.currentInstanceUpdateKey = updateKey;
155-
this.hasLoaded = false;
156-
this.error = null;
157-
this.current = null;
158-
this.chartData = [];
159-
this.shouldFetchCacheSize = true;
160-
this.shouldFetchCacheHits = true;
161-
this.shouldFetchCacheMisses = true;
143+
initCacheMetrics() {
144+
const updateKey =
145+
this.instance.version ??
146+
this.instance.statusTimestamp ??
147+
this.instance.id;
148+
const firstInit = this.currentInstanceId === null;
149+
if (
150+
this.instance.id !== this.currentInstanceId ||
151+
updateKey !== this.currentInstanceUpdateKey
152+
) {
153+
this.currentInstanceId = this.instance.id;
154+
this.currentInstanceUpdateKey = updateKey;
155+
this.hasLoaded = false;
156+
this.error = null;
157+
this.current = null;
158+
this.chartData = [];
159+
this.shouldFetchCacheSize = true;
160+
this.shouldFetchCacheHits = true;
161+
this.shouldFetchCacheMisses = true;
162162
163-
// Restart polling immediately so SSE updates refresh the view.
164-
if (!firstInit) {
165-
// Stop old subscription and start fresh
166-
this.unsubscribe();
167-
// Recreate destroy$ so new subscription can use takeUntil properly
168-
this.destroy$ = new Subject();
169-
this.subscribe();
170-
}
171-
}
172-
},
163+
// Restart polling immediately so SSE updates refresh the view.
164+
if (!firstInit) {
165+
// Stop old subscription and start fresh
166+
this.unsubscribe();
167+
// Recreate destroy$ so new subscription can use takeUntil properly
168+
this.destroy$ = new Subject();
169+
this.subscribe();
170+
}
171+
}
172+
},
173173
async fetchMetrics() {
174174
const [hit, miss, size] = await Promise.all([
175175
this.fetchCacheHits(),
@@ -250,33 +250,33 @@ export default {
250250
251251
return { ...data, hitsPerInterval, missesPerInterval, totalPerInterval };
252252
},
253-
createSubscription() {
254-
return timer(0, sbaConfig.uiSettings.pollTimer.cache)
255-
.pipe(
256-
concatMap(this.fetchMetrics),
257-
map(this.calculateMetricsPerInterval),
258-
// Stop polling when destroy$ emits (on unmount or instance update)
259-
takeUntil(this.destroy$),
260-
retryWhen((err) => {
261-
return err.pipe(delay(1000), take(5));
262-
}),
263-
)
264-
.subscribe({
265-
next: (data) => {
266-
this.hasLoaded = true;
267-
this.current = data;
268-
this.chartData.push({ ...data, timestamp: moment().valueOf() });
269-
},
270-
error: (error) => {
271-
this.hasLoaded = true;
272-
console.warn(
273-
`Fetching cache ${this.cacheName} metrics failed:`,
274-
error,
275-
);
276-
this.error = error;
277-
},
278-
});
279-
},
253+
createSubscription() {
254+
return timer(0, sbaConfig.uiSettings.pollTimer.cache)
255+
.pipe(
256+
concatMap(this.fetchMetrics),
257+
map(this.calculateMetricsPerInterval),
258+
// Stop polling when destroy$ emits (on unmount or instance update)
259+
takeUntil(this.destroy$),
260+
retryWhen((err) => {
261+
return err.pipe(delay(1000), take(5));
262+
}),
263+
)
264+
.subscribe({
265+
next: (data) => {
266+
this.hasLoaded = true;
267+
this.current = data;
268+
this.chartData.push({ ...data, timestamp: moment().valueOf() });
269+
},
270+
error: (error) => {
271+
this.hasLoaded = true;
272+
console.warn(
273+
`Fetching cache ${this.cacheName} metrics failed:`,
274+
error,
275+
);
276+
this.error = error;
277+
},
278+
});
279+
},
280280
},
281281
};
282282
</script>

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

Lines changed: 52 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -105,33 +105,33 @@ export default {
105105
},
106106
},
107107
methods: {
108-
initDatasourceMetrics() {
109-
const updateKey =
110-
this.instance.version ??
111-
this.instance.statusTimestamp ??
112-
this.instance.id;
113-
const firstInit = this.currentInstanceId === null;
114-
if (
115-
this.instance.id !== this.currentInstanceId ||
116-
updateKey !== this.currentInstanceUpdateKey
117-
) {
118-
this.currentInstanceId = this.instance.id;
119-
this.currentInstanceUpdateKey = updateKey;
120-
this.hasLoaded = false;
121-
this.error = null;
122-
this.current = null;
123-
this.chartData = [];
108+
initDatasourceMetrics() {
109+
const updateKey =
110+
this.instance.version ??
111+
this.instance.statusTimestamp ??
112+
this.instance.id;
113+
const firstInit = this.currentInstanceId === null;
114+
if (
115+
this.instance.id !== this.currentInstanceId ||
116+
updateKey !== this.currentInstanceUpdateKey
117+
) {
118+
this.currentInstanceId = this.instance.id;
119+
this.currentInstanceUpdateKey = updateKey;
120+
this.hasLoaded = false;
121+
this.error = null;
122+
this.current = null;
123+
this.chartData = [];
124124
125-
// Restart polling immediately so SSE updates refresh the view.
126-
if (!firstInit) {
127-
// Stop old subscription and start fresh
128-
this.unsubscribe();
129-
// Recreate destroy$ so new subscription can use takeUntil properly
130-
this.destroy$ = new Subject();
131-
this.subscribe();
132-
}
133-
}
134-
},
125+
// Restart polling immediately so SSE updates refresh the view.
126+
if (!firstInit) {
127+
// Stop old subscription and start fresh
128+
this.unsubscribe();
129+
// Recreate destroy$ so new subscription can use takeUntil properly
130+
this.destroy$ = new Subject();
131+
this.subscribe();
132+
}
133+
}
134+
},
135135
async fetchMetrics() {
136136
const responseActive = this.instance.fetchMetric(
137137
'jdbc.connections.active',
@@ -150,32 +150,32 @@ export default {
150150
max: (await responseMax).data.measurements[0].value,
151151
};
152152
},
153-
createSubscription() {
154-
return timer(0, sbaConfig.uiSettings.pollTimer.datasource)
155-
.pipe(
156-
concatMap(this.fetchMetrics),
157-
// Stop polling when destroy$ emits (on unmount or instance update)
158-
takeUntil(this.destroy$),
159-
retryWhen((err) => {
160-
return err.pipe(delay(1000), take(5));
161-
}),
162-
)
163-
.subscribe({
164-
next: (data) => {
165-
this.hasLoaded = true;
166-
this.current = data;
167-
this.chartData.push({ ...data, timestamp: moment().valueOf() });
168-
},
169-
error: (error) => {
170-
this.hasLoaded = true;
171-
console.warn(
172-
`Fetching datasource ${this.dataSource} metrics failed:`,
173-
error,
174-
);
175-
this.error = error;
176-
},
177-
});
178-
},
153+
createSubscription() {
154+
return timer(0, sbaConfig.uiSettings.pollTimer.datasource)
155+
.pipe(
156+
concatMap(this.fetchMetrics),
157+
// Stop polling when destroy$ emits (on unmount or instance update)
158+
takeUntil(this.destroy$),
159+
retryWhen((err) => {
160+
return err.pipe(delay(1000), take(5));
161+
}),
162+
)
163+
.subscribe({
164+
next: (data) => {
165+
this.hasLoaded = true;
166+
this.current = data;
167+
this.chartData.push({ ...data, timestamp: moment().valueOf() });
168+
},
169+
error: (error) => {
170+
this.hasLoaded = true;
171+
console.warn(
172+
`Fetching datasource ${this.dataSource} metrics failed:`,
173+
error,
174+
);
175+
this.error = error;
176+
},
177+
});
178+
},
179179
},
180180
};
181181
</script>

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,27 @@ describe('DetailsHealth', () => {
242242
}),
243243
).toBeNull();
244244
});
245+
246+
it('should fetch health details only once on startup', async () => {
247+
const application = new Application(applications[0]);
248+
const instance = application.instances[0];
249+
250+
render(DetailsHealth, {
251+
props: {
252+
instance,
253+
},
254+
});
255+
256+
await waitFor(() => {
257+
expect(healthHandlerSpy).toHaveBeenCalledTimes(1);
258+
});
259+
260+
// Verify that the handler is not called again after the initial fetch
261+
await waitFor(
262+
() => {
263+
expect(healthHandlerSpy).toHaveBeenCalledTimes(1);
264+
},
265+
{ timeout: 1000 },
266+
);
267+
});
245268
});

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ export default {
133133
methods: {
134134
reloadHealth() {
135135
const updateKey =
136-
this.instance.version ?? this.instance.statusTimestamp ?? this.instance.id;
136+
this.instance.version ??
137+
this.instance.statusTimestamp ??
138+
this.instance.id;
137139
if (
138140
this.instance.id !== this.currentInstanceId ||
139141
updateKey !== this.currentInstanceUpdateKey
@@ -167,7 +169,9 @@ export default {
167169
168170
this.currentInstanceId = this.instance.id;
169171
this.currentInstanceUpdateKey =
170-
this.instance.version ?? this.instance.statusTimestamp ?? this.instance.id;
172+
this.instance.version ??
173+
this.instance.statusTimestamp ??
174+
this.instance.id;
171175
this.liveHealth = res.data;
172176
173177
if (Array.isArray(res.data.groups)) {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ async function fetchInfo() {
7878
7979
currentInstanceId.value = props.instance.id;
8080
currentInstanceUpdateKey.value =
81-
props.instance.version ?? props.instance.statusTimestamp ?? props.instance.id;
81+
props.instance.version ??
82+
props.instance.statusTimestamp ??
83+
props.instance.id;
8284
loading.value = true;
8385
error.value = null;
8486

spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-memory.spec.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,11 @@ describe('DetailsMemory', () => {
199199
const instance1 = new Instance({
200200
id: '1',
201201
version: 1,
202-
availableMetrics: ['jvm.memory.used', 'jvm.memory.max', 'jvm.memory.committed'],
202+
availableMetrics: [
203+
'jvm.memory.used',
204+
'jvm.memory.max',
205+
'jvm.memory.committed',
206+
],
203207
});
204208
instance1.fetchMetric = vi.fn().mockImplementation((name: string) => {
205209
if (name === 'jvm.memory.max') return pMax1.promise;
@@ -211,7 +215,11 @@ describe('DetailsMemory', () => {
211215
const instance2 = new Instance({
212216
id: '1',
213217
version: 2,
214-
availableMetrics: ['jvm.memory.used', 'jvm.memory.max', 'jvm.memory.committed'],
218+
availableMetrics: [
219+
'jvm.memory.used',
220+
'jvm.memory.max',
221+
'jvm.memory.committed',
222+
],
215223
});
216224
instance2.fetchMetric = vi.fn().mockImplementation((name: string) => {
217225
const mk = (value: number) =>

spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-threads.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ describe('DetailsThreads', () => {
124124
it('should reinitialize metrics when instance version changes (SSE update)', async () => {
125125
const stubChart = {
126126
props: ['data'],
127-
template: `<div data-testid="chart">{{ JSON.stringify($props.data) }}</div>`,
127+
template:
128+
'<div data-testid="chart">{{ JSON.stringify($props.data) }}</div>',
128129
};
129130

130131
const application = new Application(applications[0]);

0 commit comments

Comments
 (0)