diff --git a/spring-boot-admin-docs/src/site/asciidoc/customize_ui.adoc b/spring-boot-admin-docs/src/site/asciidoc/customize_ui.adoc index 15366318cd5..e09bd4f8e06 100644 --- a/spring-boot-admin-docs/src/site/asciidoc/customize_ui.adoc +++ b/spring-boot-admin-docs/src/site/asciidoc/customize_ui.adoc @@ -216,3 +216,19 @@ You can very simply hide views in the navbar: ---- include::{samples-dir}/spring-boot-admin-sample-servlet/src/main/resources/application.yml[tags=customization-view-settings] ---- + +== Hide Service URL == +To hide service URLs in Spring Boot Admin UI entirely, set the following property in your Server's configuration: + +|=== +| Property name | Default | Usage + +| `spring.boot.admin.ui.show-instance-url` +| `true` +| Set to `false` to hide service URLs as well as actions that require them in UI (e.g. jump to /health or /actuator). + +|=== + +If you want to hide the URL for specific instances oncly, you can set the `hide-url` property in the instance metadata while registering a service. +When using Spring Boot Admin Client you can set the property `spring.boot.admin.client.metadata.hide-url=true` in the corresponding config file. +The value set in `metadata` does not have any effect, when the URLs are disabled in Server. diff --git a/spring-boot-admin-server-ui/src/main/frontend/global.d.ts b/spring-boot-admin-server-ui/src/main/frontend/global.d.ts index c475056a935..ef21de9eabd 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/global.d.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/global.d.ts @@ -20,7 +20,8 @@ declare global { type UITheme = { color: string; - palette: { + backgroundEnabled: boolean; + palette?: { shade50: string; shade100: string; shade200: string; @@ -60,11 +61,10 @@ declare global { type UISettings = { title: string; brand: string; - loginIcon: string; favicon: string; faviconDanger: string; pollTimer: PollTimer; - uiTheme: UITheme; + theme: UITheme; notificationFilterEnabled: boolean; rememberMeEnabled: boolean; availableLanguages: string[]; @@ -72,6 +72,7 @@ declare global { externalViews: ExternalView[]; viewSettings: ViewSettings[]; enableToasts: boolean; + showInstanceUrl: boolean; }; type SBASettings = { @@ -81,8 +82,8 @@ declare global { [key: string]: any; }; extensions: { - js: Extension[]; - css: Extension[]; + js?: Extension[]; + css?: Extension[]; }; csrf: { headerName: string; diff --git a/spring-boot-admin-server-ui/src/main/frontend/sba-config.ts b/spring-boot-admin-server-ui/src/main/frontend/sba-config.ts index 0fe5717c31d..3abbf4c3660 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/sba-config.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/sba-config.ts @@ -18,17 +18,16 @@ import { merge } from 'lodash-es'; const brand = 'Spring Boot Admin'; -const DEFAULT_CONFIG = { +const DEFAULT_CONFIG: SBASettings = { uiSettings: { + title: 'Spring Boot Admin', brand, theme: { backgroundEnabled: true, color: '#42d3a5', }, - notifications: { - enabled: true, - }, rememberMeEnabled: true, + enableToasts: false, externalViews: [] as ExternalView[], favicon: 'assets/img/favicon.png', faviconDanger: 'assets/img/favicon-danger.png', @@ -45,9 +44,10 @@ const DEFAULT_CONFIG = { threads: 2500, logfile: 1000, }, + showInstanceUrl: true, }, user: null, - extensions: [], + extensions: {}, csrf: { parameterName: '_csrf', headerName: 'X-XSRF-TOKEN', @@ -57,10 +57,14 @@ const DEFAULT_CONFIG = { }, }; -const mergedConfig = merge(DEFAULT_CONFIG, window.SBA); +const mergedConfig = merge(DEFAULT_CONFIG, window.SBA) as SBASettings; export const getCurrentUser = () => { return mergedConfig.user; }; export default mergedConfig; + +export const useSbaConfig = () => { + return mergedConfig; +}; diff --git a/spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts new file mode 100644 index 00000000000..25b89b70df8 --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts @@ -0,0 +1,43 @@ +import { describe, expect, test, vi } from 'vitest'; + +import Instance from '@/services/instance'; + +const { useSbaConfig } = vi.hoisted(() => ({ + useSbaConfig: vi.fn().mockReturnValue(true), +})); + +vi.mock('@/sba-config', async (importOriginal) => ({ + ...(await importOriginal()), + useSbaConfig, +})); + +describe('Instance', () => { + test.each` + showInstanceUrl | metadataHideUrl | expected + ${true} | ${'true'} | ${true} + ${false} | ${'true'} | ${false} + ${true} | ${'false'} | ${false} + ${false} | ${'false'} | ${false} + ${true} | ${undefined} | ${true} + `( + 'showUrl when showInstanceUrl=$showInstanceUrl and metadataHideUrl=$metadataHideUrl should return $expected', + ({ showInstanceUrl, metadataHideUrl, expected }) => { + useSbaConfig.mockReturnValue({ + uiSettings: { + showInstanceUrl: showInstanceUrl, + }, + }); + + const instance = new Instance({ + id: 'id', + registration: { + metadata: { + ['hide-url']: metadataHideUrl, + }, + }, + }); + + expect(instance.showUrl()).toBe(expected); + }, + ); +}); diff --git a/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts b/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts index 9f537913791..66aab56f772 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts @@ -25,6 +25,8 @@ import waitForPolyfill from '../utils/eventsource-polyfill'; import logtail from '../utils/logtail'; import uri from '../utils/uri'; +import { useSbaConfig } from '@/sba-config'; + const actuatorMimeTypes = [ 'application/vnd.spring-boot.actuator.v2+json', 'application/vnd.spring-boot.actuator.v1+json', @@ -117,6 +119,16 @@ class Instance { })); } + showUrl() { + const sbaConfig = useSbaConfig(); + if (sbaConfig.uiSettings.showInstanceUrl) { + const hideUrlMetadata = this.registration.metadata?.['hide-url']; + return hideUrlMetadata === undefined || hideUrlMetadata === 'true'; + } + + return false; + } + getId() { return this.id; } diff --git a/spring-boot-admin-server-ui/src/main/frontend/shell/navbar.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/shell/navbar.spec.ts index 3f4c27d5867..dbe5b978b45 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/shell/navbar.spec.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/shell/navbar.spec.ts @@ -37,7 +37,7 @@ describe('Navbar', function () { }); render(Navbar); - screen.logTestingPlaygroundURL(); + expect(screen.getByTestId('usermenu')).toBeVisible(); expect(screen.getByText('mail@example.org')).toBeVisible(); }); diff --git a/spring-boot-admin-server-ui/src/main/frontend/tests/setup.ts b/spring-boot-admin-server-ui/src/main/frontend/tests/setup.ts index 6801789c0c0..9bf0898bf7e 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/tests/setup.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/tests/setup.ts @@ -1,8 +1,10 @@ +import '@testing-library/jest-dom'; import '@testing-library/jest-dom/vitest'; import { cleanup } from '@testing-library/vue'; import { afterAll, afterEach, beforeAll, vi } from 'vitest'; import { server } from '@/mocks/server'; +import sbaConfig from '@/sba-config'; global.IntersectionObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), @@ -15,6 +17,8 @@ global.ResizeObserver = vi.fn().mockImplementation(() => ({ disconnect: vi.fn(), })); +global.SBA = sbaConfig; + beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); afterAll(() => server.close()); afterEach(() => server.resetHandlers()); diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/applications/InstancesList.vue b/spring-boot-admin-server-ui/src/main/frontend/views/applications/InstancesList.vue index ef3ce04328d..ac4ab09614b 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/applications/InstancesList.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/views/applications/InstancesList.vue @@ -1,5 +1,5 @@