From 80c754240036691a0591f4929331a42b97e5a1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Ko=CC=88ninger?= Date: Fri, 15 May 2026 21:41:47 +0200 Subject: [PATCH 1/7] feat(health-details): add collapsible health details with localStorage persistence --- .../instances/details/details-health.vue | 8 +- .../instances/details/health-details.spec.ts | 300 ++++++++++++++++++ .../instances/details/health-details.vue | 97 ++++-- .../views/instances/details/i18n.de.json | 3 +- .../views/instances/details/i18n.en.json | 3 +- .../views/instances/details/i18n.es.json | 3 +- .../views/instances/details/i18n.fr.json | 3 +- .../views/instances/details/i18n.is.json | 3 +- .../views/instances/details/i18n.ko.json | 3 +- .../views/instances/details/i18n.pt-BR.json | 3 +- .../views/instances/details/i18n.ru.json | 3 +- .../views/instances/details/i18n.zh-CN.json | 3 +- .../views/instances/details/i18n.zh-TW.json | 3 +- 13 files changed, 396 insertions(+), 39 deletions(-) diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-health.vue b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-health.vue index e5527a4ce70..fab7912d158 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-health.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-health.vue @@ -48,7 +48,7 @@ :title="$t('term.fetch_failed')" />
- +
diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.spec.ts index 9b37ddd4b61..06c8fe5d3cc 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.spec.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.spec.ts @@ -1,10 +1,16 @@ +import userEvent from '@testing-library/user-event'; import { screen, within } from '@testing-library/vue'; import { beforeEach, describe, expect, it } from 'vitest'; +import Instance from '@/services/instance'; import { render } from '@/test-utils'; import HealthDetails from '@/views/instances/details/health-details.vue'; describe('HealthDetails', () => { + const mockInstance = { + id: 'test-instance-123', + } as Instance; + describe('Health .details', () => { beforeEach(() => { const healthMock = { @@ -64,6 +70,7 @@ describe('HealthDetails', () => { props: { name: 'Name', health: healthMock, + instance: mockInstance, }, }); }); @@ -156,6 +163,8 @@ describe('HealthDetails', () => { render(HealthDetails, { props: { health: healthMock, + name: 'root', + instance: mockInstance, }, }); }); @@ -184,4 +193,295 @@ describe('HealthDetails', () => { }, ); }); + + describe('Collapsible details functionality', () => { + beforeEach(() => { + // Clear localStorage before each test + localStorage.clear(); + }); + + it('should show toggle button when details exist', async () => { + const healthMock = { + status: 'UP', + details: { + database: 'HSQL Database Engine', + validationQuery: 'isValid()', + }, + }; + + render(HealthDetails, { + props: { + name: 'db', + health: healthMock, + instance: mockInstance, + }, + }); + + const toggleButton = await screen.findByRole('button'); + expect(toggleButton).toBeInTheDocument(); + expect(toggleButton).toHaveAttribute('title', 'Toggle db health details'); + }); + + it('should have proper ARIA attributes on toggle button', async () => { + const healthMock = { + status: 'UP', + details: { + database: 'HSQL Database Engine', + }, + }; + + render(HealthDetails, { + props: { + name: 'db', + health: healthMock, + instance: mockInstance, + }, + }); + + const toggleButton = await screen.findByRole('button'); + + // Should have title with translated text + expect(toggleButton).toHaveAttribute('title', 'Toggle db health details'); + + // Should have aria-expanded set to false initially (collapsed) + expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); + + // Should have aria-controls pointing to the details element + expect(toggleButton).toHaveAttribute('aria-controls'); + const controlsId = toggleButton.getAttribute('aria-controls'); + expect(controlsId).toContain('health-details'); + expect(controlsId).toContain('db'); + }); + + it('should update aria-expanded when toggled', async () => { + const user = userEvent.setup(); + const healthMock = { + status: 'UP', + details: { + database: 'HSQL Database Engine', + }, + }; + + render(HealthDetails, { + props: { + name: 'db', + health: healthMock, + instance: mockInstance, + }, + }); + + const toggleButton = await screen.findByRole('button'); + + // Initially collapsed + expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); + + // After clicking - expanded + await user.click(toggleButton); + expect(toggleButton).toHaveAttribute('aria-expanded', 'true'); + + // After clicking again - collapsed + await user.click(toggleButton); + expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); + }); + + it('should not show toggle button when no details exist', async () => { + const healthMock = { + status: 'UP', + }; + + render(HealthDetails, { + props: { + name: 'simple', + health: healthMock, + instance: mockInstance, + }, + }); + + const toggleButton = screen.queryByRole('button'); + expect(toggleButton).not.toBeInTheDocument(); + }); + + it('should start collapsed by default', async () => { + const healthMock = { + status: 'UP', + details: { + database: 'HSQL Database Engine', + validationQuery: 'isValid()', + }, + }; + + render(HealthDetails, { + props: { + name: 'db', + health: healthMock, + instance: mockInstance, + }, + }); + + // Details should not be visible initially + expect( + screen.queryByText('HSQL Database Engine'), + ).not.toBeInTheDocument(); + }); + + it('should expand details when toggle button is clicked', async () => { + const user = userEvent.setup(); + const healthMock = { + status: 'UP', + details: { + database: 'HSQL Database Engine', + validationQuery: 'isValid()', + }, + }; + + render(HealthDetails, { + props: { + name: 'db', + health: healthMock, + instance: mockInstance, + }, + }); + + const toggleButton = await screen.findByRole('button'); + await user.click(toggleButton); + + // Details should now be visible + expect( + await screen.findByText('HSQL Database Engine'), + ).toBeInTheDocument(); + expect(screen.getByText('isValid()')).toBeInTheDocument(); + }); + + it('should collapse details when toggle button is clicked twice', async () => { + const user = userEvent.setup(); + const healthMock = { + status: 'UP', + details: { + database: 'HSQL Database Engine', + }, + }; + + render(HealthDetails, { + props: { + name: 'db', + health: healthMock, + instance: mockInstance, + }, + }); + + const toggleButton = await screen.findByRole('button'); + + // First click - expand + await user.click(toggleButton); + expect( + await screen.findByText('HSQL Database Engine'), + ).toBeInTheDocument(); + + // Second click - collapse + await user.click(toggleButton); + expect( + screen.queryByText('HSQL Database Engine'), + ).not.toBeInTheDocument(); + }); + + it('should persist collapsed state in localStorage', async () => { + const user = userEvent.setup(); + const healthMock = { + status: 'UP', + details: { + database: 'HSQL Database Engine', + }, + }; + + render(HealthDetails, { + props: { + name: 'db', + health: healthMock, + instance: mockInstance, + }, + }); + + const toggleButton = await screen.findByRole('button'); + await user.click(toggleButton); + + const storageKey = `de.codecentric.spring-boot-admin.health-details.db.${mockInstance.id}.collapsed`; + expect(localStorage.getItem(storageKey)).toBe('false'); + }); + + it('should restore collapsed state from localStorage', async () => { + const healthMock = { + status: 'UP', + details: { + database: 'HSQL Database Engine', + }, + }; + + const storageKey = `de.codecentric.spring-boot-admin.health-details.db.${mockInstance.id}.collapsed`; + localStorage.setItem(storageKey, 'false'); + + render(HealthDetails, { + props: { + name: 'db', + health: healthMock, + instance: mockInstance, + }, + }); + + // Details should be visible because we set collapsed to false in localStorage + expect( + await screen.findByText('HSQL Database Engine'), + ).toBeInTheDocument(); + }); + + it('should handle child health components correctly', async () => { + const user = userEvent.setup(); + const healthMock = { + status: 'UP', + components: { + db: { + status: 'UP', + details: { + database: 'HSQL Database Engine', + }, + }, + }, + }; + + render(HealthDetails, { + props: { + name: 'parent', + health: healthMock, + instance: mockInstance, + }, + }); + + // Child component should be rendered + const dbComponent = await screen.findByRole('definition', { name: 'db' }); + expect(dbComponent).toBeInTheDocument(); + }); + + it('should not show toggle button when details only contain child health components', async () => { + const healthMock = { + status: 'UP', + details: { + childComponent: { + status: 'UP', + details: {}, + }, + }, + }; + + render(HealthDetails, { + props: { + name: 'parent', + health: healthMock, + instance: mockInstance, + }, + }); + + // No toggle button because all details are child health components + const toggleButton = screen.queryByRole('button'); + expect(toggleButton).not.toBeInTheDocument(); + }); + }); }); diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.vue b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.vue index d60486b2114..0256e9d4e35 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.vue @@ -15,24 +15,45 @@ --> diff --git a/spring-boot-admin-server-ui/src/main/frontend/test-utils.ts b/spring-boot-admin-server-ui/src/main/frontend/test-utils.ts index dfa598da467..ffcf1d48a89 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/test-utils.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/test-utils.ts @@ -18,7 +18,8 @@ const modules: Record = import.meta.glob('@/**/i18n.en.json', { eager: true, }); for (const modulesKey in modules) { - terms = { ...terms, ...modules[modulesKey] }; + const moduleContent = modules[modulesKey].default || modules[modulesKey]; + terms = { ...terms, ...moduleContent }; } export let router; createViewRegistry(); diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/__snapshots__/health-details.spec.ts.snap b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/__snapshots__/health-details.spec.ts.snap index 0ad277d1391..673cf2a8713 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/__snapshots__/health-details.spec.ts.snap +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/__snapshots__/health-details.spec.ts.snap @@ -2,8 +2,11 @@ exports[`HealthDetails > Health .details > should format object details correctly 1`] = `
 {
 
   describe('Health .details', () => {
     beforeEach(() => {
+      // Clear localStorage and set all details to expanded for these tests
+      localStorage.clear();
+      // These tests expect details to be visible by default
+      // We need to set localStorage for all health components that will be rendered
+      const componentsToExpand = [
+        'clientConfigServer',
+        'db',
+        'discoveryComposite',
+        'discoveryClient',
+        'diskSpace',
+        'diskSpace2',
+        'ssl',
+      ];
+      componentsToExpand.forEach((name) => {
+        localStorage.setItem(
+          `de.codecentric.spring-boot-admin.health-details.${name}.test-instance-123.collapsed`,
+          'false',
+        );
+      });
+
       const healthMock = {
         status: 'UP',
         details: {
@@ -131,6 +151,22 @@ describe('HealthDetails', () => {
 
   describe('Health .components', () => {
     beforeEach(() => {
+      // Clear localStorage and set all components to expanded for these tests
+      localStorage.clear();
+      // These tests expect components to be visible by default
+      const componentsToExpand = [
+        'clientConfigServer',
+        'discoveryComposite',
+        'discoveryClient',
+        'diskSpace',
+      ];
+      componentsToExpand.forEach((name) => {
+        localStorage.setItem(
+          `de.codecentric.spring-boot-admin.health-details.${name}.test-instance-123.collapsed`,
+          'false',
+        );
+      });
+
       const healthMock = {
         status: 'UP',
         components: {
@@ -219,7 +255,10 @@ describe('HealthDetails', () => {
 
       const toggleButton = await screen.findByRole('button');
       expect(toggleButton).toBeInTheDocument();
-      expect(toggleButton).toHaveAttribute('title', 'Toggle db health details');
+      expect(toggleButton).toHaveAttribute('title');
+      // Title should be the i18n key for toggle_details
+      const title = toggleButton.getAttribute('title');
+      expect(title).toContain('toggle_details');
     });
 
     it('should have proper ARIA attributes on toggle button', async () => {
@@ -240,8 +279,10 @@ describe('HealthDetails', () => {
 
       const toggleButton = await screen.findByRole('button');
 
-      // Should have title with translated text
-      expect(toggleButton).toHaveAttribute('title', 'Toggle db health details');
+      // Should have title with toggle_details i18n key
+      expect(toggleButton).toHaveAttribute('title');
+      const title = toggleButton.getAttribute('title');
+      expect(title).toContain('toggle_details');
 
       // Should have aria-expanded set to false initially (collapsed)
       expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
@@ -434,7 +475,6 @@ describe('HealthDetails', () => {
     });
 
     it('should handle child health components correctly', async () => {
-      const user = userEvent.setup();
       const healthMock = {
         status: 'UP',
         components: {
diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.vue b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.vue
index 0256e9d4e35..6ff7164ec54 100644
--- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.vue
+++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.vue
@@ -15,7 +15,11 @@
   -->