Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
537eeee
#5352: Cache health groups and expose via health-groups endpoint
ulischulte Jun 5, 2026
c0c3dbf
#5352: Cleanup health groups cache
ulischulte Jun 5, 2026
58d8efd
Update spring-boot-admin-server/src/main/java/de/codecentric/boot/adm…
ulischulte Jun 12, 2026
e0a1510
Fix renaming of method
ulischulte Jun 12, 2026
95f761b
#5352: Define interface for HealthGroupsCache
ulischulte Jun 12, 2026
ffe3250
#5352: Re-fetch the (server-cached) group list on every instance change
ulischulte Jun 12, 2026
b469f7a
chore: update project version to 4.1.1-SNAPSHOT in pom.xml
SteKoe Jun 19, 2026
475ab1d
feat(server): Add messageThreadId handling in super groups for Telegr…
AliArtukov Jun 19, 2026
2ff8c42
chore: update project version to 4.1.2-SNAPSHOT in pom.xml
SteKoe Jun 19, 2026
5691298
fix(deps): update dependency axios to v1.18.0 (#5466)
renovate[bot] Jun 19, 2026
f6c5b77
chore(deps): update storybook monorepo to v10.4.5 (#5467)
renovate[bot] Jun 20, 2026
9f572bf
chore(deps): update vitest monorepo to v4.1.9 (#5468)
renovate[bot] Jun 20, 2026
fcc8b99
chore(deps): update typescript-eslint monorepo to v8.61.1 (#5469)
renovate[bot] Jun 20, 2026
bea4298
chore(deps): update happy-dom monorepo to v20.10.4 (#5470)
renovate[bot] Jun 21, 2026
efe9d90
chore(deps): update storybook monorepo to v10.4.6 (#5471)
renovate[bot] Jun 21, 2026
054d3bf
chore(deps): update central-publishing-maven-plugin.version to v0.11.…
renovate[bot] Jun 21, 2026
5c30e6b
chore(deps): update happy-dom monorepo to v20.10.5 (#5473)
renovate[bot] Jun 22, 2026
70c0331
chore(deps): update node.js to v24.17.0 (#5474)
renovate[bot] Jun 22, 2026
37c8c21
chore(deps): update node.js version to v24.17.0 (#5475)
renovate[bot] Jun 22, 2026
f58b4b5
fix(deps): update dependency vue-i18n to v11.4.6 (#5479)
renovate[bot] Jun 23, 2026
6eeb2ff
chore(deps): update happy-dom monorepo (#5478)
renovate[bot] Jun 23, 2026
51698f5
chore(deps): update dependency cronstrue to v3.20.0 (#5480)
renovate[bot] Jun 23, 2026
2df8e89
chore(deps): update dependency org.cyclonedx:cyclonedx-maven-plugin t…
renovate[bot] Jun 26, 2026
7fa7793
chore(deps): update dependency cronstrue to v3.21.0 (#5486)
renovate[bot] Jun 27, 2026
2848a65
chore(deps): update dependency globals to v17.7.0 (#5487)
renovate[bot] Jun 27, 2026
59da836
fix(deps): update dependency axios to v1.18.1 (#5488)
renovate[bot] Jun 27, 2026
26949b3
chore(deps): update typescript-eslint monorepo to v8.62.0 (#5489)
renovate[bot] Jun 27, 2026
e0135aa
chore(deps): update node.js to v24.18.0 (#5490)
renovate[bot] Jun 28, 2026
89cdcd0
chore(deps): update node.js to v24.18.0 (#5491)
renovate[bot] Jun 28, 2026
fa09e9f
chore(deps): update dependency vite to v8.1.0 (#5493)
renovate[bot] Jun 28, 2026
e075b45
refactor(viewRegistry): replace shallowRef with defineComponent for i…
SteKoe Jun 29, 2026
b01c2d0
chore(deps): add vite-plugin-vue-devtools and update dependencies in …
SteKoe Jun 29, 2026
76886db
refactor(index): specify views type as DefineComponent for better typ…
SteKoe Jun 29, 2026
d260a11
refactor(hex-mesh): optimize font size update and improve layout calc…
SteKoe Jun 29, 2026
c90f9a4
refactor(hex-mesh): streamline layout calculation by consolidating pa…
SteKoe Jul 1, 2026
c7106c4
fix(deps): update dependency js-yaml to v5 (#5484)
renovate[bot] Jul 1, 2026
9e49902
chore(deps): update actions/checkout action to v7 (#5477)
renovate[bot] Jul 1, 2026
4aa9d39
chore(deps): update actions/cache action to v6 (#5494)
renovate[bot] Jul 1, 2026
383b32e
chore(tests): add regression tests for UiController (#5481)
amirdeljouyi Jul 1, 2026
1f03598
chore(deps): update dependency org.graalvm.buildtools:native-maven-pl…
renovate[bot] Jul 1, 2026
ad4f738
fix(deps): update font awesome to v7.3.0 (#5498)
renovate[bot] Jul 1, 2026
04b71eb
fix(deps): update dependency vue to v3.5.39 (#5497)
renovate[bot] Jul 1, 2026
fbd1014
fix(deps): update dependency asciidoctor to v4 (#5492)
renovate[bot] Jul 1, 2026
e5997f7
chore(deps): update dependency qs to v6.15.3 (#5496)
renovate[bot] Jul 1, 2026
11559e9
chore(deps): update dependency prettier to v3.8.5 (#5499)
renovate[bot] Jul 1, 2026
70ef150
fix(deps): update dependency iso8601-duration to v2.1.4 (#5500)
renovate[bot] Jul 1, 2026
570d8a1
chore(deps): update dependency js-yaml to v5.2.0 (#5502)
renovate[bot] Jul 2, 2026
ff61ac6
chore(deps): update dependency eslint to v10.6.0 (#5501)
renovate[bot] Jul 2, 2026
6530b56
fix(deps): update dependency @fortawesome/vue-fontawesome to v3.3.0 (…
renovate[bot] Jul 2, 2026
727f883
chore(deps): update dependency vite-plugin-vue-devtools to v8.1.5 (#5…
renovate[bot] Jul 3, 2026
6b1b4d6
#5352: Replaced the old trigger-based pattern with an event listener,…
ulischulte Jul 3, 2026
670eec9
Merge branch 'master' into feature/5352-healthgroups-endpoint
ulischulte Jul 3, 2026
ac792dd
Merge remote-tracking branch 'origin/master' into feature/5352-health…
ulischulte Jul 3, 2026
5aa6808
apply changes from master
ulischulte Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 33 additions & 36 deletions spring-boot-admin-server-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AxiosError, AxiosInstance } from 'axios';
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import saveAs from 'file-saver';
import { Observable, concat, from, ignoreElements } from 'rxjs';

Expand Down Expand Up @@ -231,6 +231,10 @@ class Instance {
});
}

async fetchCachedHealthGroups(): Promise<AxiosResponse<string[]>> {
return this.axios.get<string[]>('health-groups');
}

async fetchHealthGroup(groupName: string) {
return await this.axios.get(uri`actuator/health/${groupName}`, {
validateStatus: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ describe('DetailsHealth', () => {
const application = new Application(applications[0]);
const instance = application.instances[0];

// Mock fetchHealth for groups (will be called once on mount)
instance.fetchHealth = vi
// Mock fetchCachedHealthGroups for groups (will be called once on mount)
instance.fetchCachedHealthGroups = vi
.fn()
.mockResolvedValue({ data: { status: 'UP', groups: ['liveness'] } });
.mockResolvedValue({ data: ['liveness'] });

render(DetailsHealth, {
props: {
Expand All @@ -32,9 +32,9 @@ describe('DetailsHealth', () => {
const application = new Application(applications[0]);
const instance = application.instances[0];

instance.fetchHealth = vi
instance.fetchCachedHealthGroups = vi
.fn()
.mockResolvedValue({ data: { status: 'UP', groups: ['liveness'] } });
.mockResolvedValue({ data: ['liveness'] });

render(DetailsHealth, {
props: {
Expand All @@ -56,9 +56,9 @@ describe('DetailsHealth', () => {
it('should update when instance prop changes', async () => {
const application = new Application(applications[0]);
const instance1 = application.instances[0];
instance1.fetchHealth = vi
instance1.fetchCachedHealthGroups = vi
.fn()
.mockResolvedValue({ data: { status: 'UP', groups: ['liveness'] } });
.mockResolvedValue({ data: ['liveness'] });

const { rerender } = render(DetailsHealth, {
props: {
Expand All @@ -78,9 +78,7 @@ describe('DetailsHealth', () => {
},
],
}).instances[0];
instance2.fetchHealth = vi
.fn()
.mockResolvedValue({ data: { status: 'DOWN', groups: [] } });
instance2.fetchCachedHealthGroups = vi.fn().mockResolvedValue({ data: [] });

await rerender({ instance: instance2 });

Expand All @@ -92,9 +90,9 @@ describe('DetailsHealth', () => {
const application = new Application(applications[0]);
const instance = application.instances[0];
instance.statusInfo = { status: 'UP', details: {} };
instance.fetchHealth = vi
instance.fetchCachedHealthGroups = vi
.fn()
.mockResolvedValue({ data: { status: 'UP', groups: ['liveness'] } });
.mockResolvedValue({ data: ['liveness'] });

render(DetailsHealth, {
props: {
Expand All @@ -107,22 +105,22 @@ describe('DetailsHealth', () => {
});

describe('SSE reactive updates', () => {
it('should call fetchHealth once on mount, not on SSE version changes', async () => {
it('should re-fetch cached health groups on SSE version changes', async () => {
const baseApp = applications[0];
const instance1 = new Application(baseApp).instances[0];
const fetchHealthSpy1 = vi.spyOn(instance1, 'fetchHealth');
fetchHealthSpy1.mockResolvedValue({
data: { status: 'UP', groups: ['liveness'] },
} as AxiosResponse);
instance1.fetchCachedHealthGroups = vi
.fn()
.mockResolvedValue({ data: ['liveness'] });

const { rerender } = render(DetailsHealth, {
props: { instance: instance1 },
});

await screen.findAllByRole('status');
expect(fetchHealthSpy1).toHaveBeenCalledTimes(1);
expect(instance1.fetchCachedHealthGroups).toHaveBeenCalledTimes(1);

// Same instance, different version (SSE update) — should NOT call fetchHealth again
// Same instance, different version (SSE update) — should re-fetch the
// (server-cached) group list so it self-corrects when groups change.
const instance2 = new Application({
...baseApp,
instances: [
Expand All @@ -133,24 +131,25 @@ describe('DetailsHealth', () => {
},
],
}).instances[0];
const fetchHealthSpy2 = vi.spyOn(instance2, 'fetchHealth');
fetchHealthSpy2.mockResolvedValue({
data: { status: 'DOWN', groups: [] },
} as AxiosResponse);
instance2.fetchCachedHealthGroups = vi
.fn()
.mockResolvedValue({ data: ['liveness', 'readiness'] });

await rerender({ instance: instance2 });

// Original instance's spy should still be 1 (no additional calls)
expect(fetchHealthSpy1).toHaveBeenCalledTimes(1);
// The new instance's cached groups should have been fetched on the SSE update.
await waitFor(() => {
expect(instance2.fetchCachedHealthGroups).toHaveBeenCalledTimes(1);
});
});

it('should reactively update through multiple SSE status changes without extra HTTP calls', async () => {
it('should reactively update health status and details through SSE status changes', async () => {
const baseApp = applications[0];

const instance1 = new Application(baseApp).instances[0];
instance1.fetchHealth = vi
instance1.fetchCachedHealthGroups = vi
.fn()
.mockResolvedValue({ data: { status: 'UP', groups: ['liveness'] } });
.mockResolvedValue({ data: ['liveness'] });

const { rerender } = render(DetailsHealth, {
props: { instance: instance1 },
Expand Down Expand Up @@ -178,9 +177,9 @@ describe('DetailsHealth', () => {
},
],
}).instances[0];
instance2.fetchHealth = vi
instance2.fetchCachedHealthGroups = vi
.fn()
.mockResolvedValue({ data: { status: 'DOWN', groups: [] } });
.mockResolvedValue({ data: [] });

await rerender({ instance: instance2 });

Expand All @@ -197,8 +196,8 @@ describe('DetailsHealth', () => {
it('should display health group buttons after mount', async () => {
const application = new Application(applications[0]);
const instance = application.instances[0];
instance.fetchHealth = vi.fn().mockResolvedValue({
data: { status: 'UP', groups: ['liveness', 'readiness'] },
instance.fetchCachedHealthGroups = vi.fn().mockResolvedValue({
data: ['liveness', 'readiness'],
});
const fetchGroupSpy = vi.spyOn(instance, 'fetchHealthGroup');

Expand All @@ -220,8 +219,8 @@ describe('DetailsHealth', () => {
it('should fetch group details on first click', async () => {
const application = new Application(applications[0]);
const instance = application.instances[0];
instance.fetchHealth = vi.fn().mockResolvedValue({
data: { status: 'UP', groups: ['custom-group'] },
instance.fetchCachedHealthGroups = vi.fn().mockResolvedValue({
data: ['custom-group'],
});
const fetchGroupSpy = vi.spyOn(instance, 'fetchHealthGroup');
fetchGroupSpy.mockResolvedValue({
Expand Down Expand Up @@ -270,8 +269,8 @@ describe('DetailsHealth', () => {
it('should toggle group visibility after data is loaded', async () => {
const application = new Application(applications[0]);
const instance = application.instances[0];
instance.fetchHealth = vi.fn().mockResolvedValue({
data: { status: 'UP', groups: ['custom-group'] },
instance.fetchCachedHealthGroups = vi.fn().mockResolvedValue({
data: ['custom-group'],
});
const fetchGroupSpy = vi.spyOn(instance, 'fetchHealthGroup');
fetchGroupSpy.mockResolvedValue({
Expand Down Expand Up @@ -309,8 +308,8 @@ describe('DetailsHealth', () => {
it('should not show groups when none exist', async () => {
const application = new Application(applications[0]);
const instance = application.instances[0];
instance.fetchHealth = vi.fn().mockResolvedValue({
data: { status: 'UP', groups: [] },
instance.fetchCachedHealthGroups = vi.fn().mockResolvedValue({
data: [],
});

render(DetailsHealth, {
Expand All @@ -326,16 +325,16 @@ describe('DetailsHealth', () => {

it('should re-fetch groups when instance id changes', async () => {
const app1 = new Application(applications[0]).instances[0];
app1.fetchHealth = vi
app1.fetchCachedHealthGroups = vi
.fn()
.mockResolvedValue({ data: { status: 'UP', groups: ['liveness'] } });
.mockResolvedValue({ data: ['liveness'] });

const { rerender } = render(DetailsHealth, {
props: { instance: app1 },
});

await waitFor(() => {
expect(app1.fetchHealth).toHaveBeenCalledTimes(1);
expect(app1.fetchCachedHealthGroups).toHaveBeenCalledTimes(1);
});

const app2 = new Application({
Expand All @@ -347,18 +346,18 @@ describe('DetailsHealth', () => {
},
],
}).instances[0];
app2.fetchHealth = vi
app2.fetchCachedHealthGroups = vi
.fn()
.mockResolvedValue({ data: { status: 'UP', groups: ['readiness'] } });
.mockResolvedValue({ data: ['readiness'] });

await rerender({ instance: app2 });

await waitFor(() => {
expect(app2.fetchHealth).toHaveBeenCalledTimes(1);
expect(app2.fetchCachedHealthGroups).toHaveBeenCalledTimes(1);
});

// Original instance should still have only 1 call
expect(app1.fetchHealth).toHaveBeenCalledTimes(1);
expect(app1.fetchCachedHealthGroups).toHaveBeenCalledTimes(1);
});
});
});
Loading
Loading