Skip to content

Commit 5bd7744

Browse files
authored
feat(#4700): add custom links on instance level via metadata (#4733)
1 parent ac365b7 commit 5bd7744

File tree

17 files changed

+293
-98
lines changed

17 files changed

+293
-98
lines changed

spring-boot-admin-docs/src/site/docs/client/80-configuration.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ a unified configuration approach across your entire system.
1616

1717
__Instance metadata options__
1818

19-
| Property name | Description |
20-
|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
21-
| `tags.*` | Tags as key-value-pairs to be associated with this instance. |
22-
| `group` | Assign a group name. Used in UI to aggregate instances not by application but by assigned group. |
23-
| `hide-url` | Hide URLs of the instance in UI. Useful, when running in a cluster, exposing a non routable URL. |
24-
| `disable-url` | Disables links of this instance in UI. Useful, when the URL does not point to a UI. |
25-
| `service-url` | Override the service url of the registered service. Allows to specify the actual URL to the UI. This does not affect management url. |
26-
| `user.name`<br/>`user.password` | Credentials being used to access the endpoints. |
19+
| Property name | Description |
20+
|------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
21+
| `disable-url` | Disables links of this instance in UI. Useful, when the URL does not point to a UI. |
22+
| `group` | Assign a group name. Used in UI to aggregate instances not by application but by assigned group. |
23+
| `hide-url` | Hide URLs of the instance in UI. Useful, when running in a cluster, exposing a non routable URL. |
24+
| `service-url` | Override the service url of the registered service. Allows to specify the actual URL to the UI. This does not affect management url. |
25+
| `sidebar.links.N.label` <br/> `sidebar.links.N.url` <br/> `sidebar.links.N.iframe` | **label:** Shown in sidebar <br/>**url:** URL used as href in link or iframe. <br/> **iframe:** boolean value that allows to include URL as iframe. |
26+
| `tags.*` | Tags as key-value-pairs to be associated with this instance. |
27+
| `user.name`<br/>`user.password` | Credentials being used to access the endpoints. |

spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ SBA.viewRegistry.addView({
8484
SBA.viewRegistry.setGroupIcon(
8585
"custom", //<1>
8686
`<svg xmlns='http://www.w3.org/2000/svg'
87-
class='h-5 mr-3'
87+
class='h-5 mr-3'
8888
viewBox='0 0 576 512'><path d='M512 80c8.8 0 16 7.2 16 16V416c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V96c0-8.8 7.2-16 16-16H512zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM200 208c14.2 0 27 6.1 35.8 16c8.8 9.9 24 10.7 33.9 1.9s10.7-24 1.9-33.9c-17.5-19.6-43.1-32-71.5-32c-53 0-96 43-96 96s43 96 96 96c28.4 0 54-12.4 71.5-32c8.8-9.9 8-25-1.9-33.9s-25-8-33.9 1.9c-8.8 9.9-21.6 16-35.8 16c-26.5 0-48-21.5-48-48s21.5-48 48-48zm144 48c0-26.5 21.5-48 48-48c14.2 0 27 6.1 35.8 16c8.8 9.9 24 10.7 33.9 1.9s10.7-24 1.9-33.9c-17.5-19.6-43.1-32-71.5-32c-53 0-96 43-96 96s43 96 96 96c28.4 0 54-12.4 71.5-32c8.8-9.9 8-25-1.9-33.9s-25-8-33.9 1.9c-8.8 9.9-21.6 16-35.8 16c-26.5 0-48-21.5-48-48z'/>
8989
</svg>` //<2>
9090
);

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable no-undef */
21
class HealthStatusEnum {
32
static DOWN = 'DOWN';
43
static UP = 'UP';

spring-boot-admin-server-ui/src/main/frontend/components/ActionScope.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable no-undef */
21
class ActionScopeEnum {
32
static INSTANCE = 'instance';
43
static APPLICATION = 'application';

spring-boot-admin-server-ui/src/main/frontend/services/instance.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import uri from '../utils/uri';
2727

2828
import { useSbaConfig } from '@/sba-config';
2929
import { actuatorMimeTypes } from '@/services/spring-mime-types';
30+
import { transformToJSON } from '@/utils/transformToJSON';
3031

3132
const isInstanceActuatorRequest = (url: string) =>
3233
url.match(/^instances[/][^/]+[/]actuator([/].*)?$/);
@@ -63,6 +64,11 @@ class Instance {
6364
return this.registration.metadata;
6465
}
6566

67+
get metadataParsed() {
68+
const metadata = this.registration.metadata || {};
69+
return transformToJSON(metadata);
70+
}
71+
6672
get isUnregisterable() {
6773
return this.registration.source === 'http-api';
6874
}

spring-boot-admin-server-ui/src/main/frontend/services/startup-actuator.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ describe('StartupActuatorService', () => {
2424
let events: any = {};
2525

2626
beforeEach(() => {
27-
// eslint-disable-next-line @typescript-eslint/no-var-requires
2827
data = cloneDeep(fixture);
2928
events = data.timeline.events;
3029
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { transformToJSON } from '@/utils/transformToJSON';
4+
5+
const input = {
6+
'service-url': 'http://localhost:8080',
7+
'sidebar.links.0.iframe': 'true',
8+
'sidebar.links.0.label': '🏠 Home',
9+
'sidebar.links.0.url': 'https://codecentric.de',
10+
'tags.environment': 'test',
11+
};
12+
13+
describe('transformToNestedPojo', () => {
14+
const output = transformToJSON(input);
15+
16+
it('transforms simple key', () => {
17+
expect(output['service-url']).toEqual('http://localhost:8080');
18+
});
19+
20+
it('transforms nested key', () => {
21+
expect(output.tags.environment).toEqual('test');
22+
});
23+
24+
it('transforms array', () => {
25+
expect(output.sidebar.links).toHaveLength(1);
26+
expect(output.sidebar.links[0]).toEqual({
27+
iframe: 'true',
28+
label: '🏠 Home',
29+
url: 'https://codecentric.de',
30+
});
31+
});
32+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Converts a flat object with dot notation keys into a nested POJO.
3+
*
4+
* @param input - The flat object to transform.
5+
* @returns The nested POJO.
6+
*
7+
* @example
8+
* transformToJSON({
9+
* 'user.name': 'Alice',
10+
* 'user.address.street': 'Main St',
11+
* 'user.address.zip': '12345',
12+
* 'items.0.id': 1,
13+
* 'items.0.name': 'Item 1',
14+
* 'items.1.id': 2,
15+
* 'items.1.name': 'Item 2'
16+
* });
17+
* Returns:
18+
* {
19+
* user: {
20+
* name: 'Alice',
21+
* address: { street: 'Main St', zip: '12345' }
22+
* },
23+
* items: [
24+
* { id: 1, name: 'Item 1' },
25+
* { id: 2, name: 'Item 2' }
26+
* ]
27+
* }
28+
*/
29+
export function transformToJSON(input: Record<string, any>): any {
30+
const result: any = {};
31+
for (const [flatKey, value] of Object.entries(input)) {
32+
const parts = flatKey.split('.');
33+
let current = result;
34+
for (let i = 0; i < parts.length; i++) {
35+
const part = parts[i];
36+
const nextPart = parts[i + 1];
37+
const isNextArrayIndex = nextPart !== undefined && /^\d+$/.test(nextPart);
38+
if (i === parts.length - 1) {
39+
current[part] = value;
40+
} else {
41+
if (/^\d+$/.test(part)) {
42+
const idx = Number(part);
43+
if (!Array.isArray(current)) {
44+
const arr: any[] = [];
45+
Object.assign(arr, current);
46+
current = arr;
47+
}
48+
if (!current[idx]) current[idx] = isNextArrayIndex ? [] : {};
49+
current = current[idx];
50+
} else {
51+
if (!current[part]) current[part] = isNextArrayIndex ? [] : {};
52+
current = current[part];
53+
}
54+
}
55+
}
56+
}
57+
return result;
58+
}

spring-boot-admin-server-ui/src/main/frontend/views/about/index.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ import handle from '@/views/about/handle.vue';
121121
export default defineComponent({
122122
components: { SbaWave, SbaButton },
123123
data: () => ({
124-
// eslint-disable-next-line no-undef
125124
version: __PROJECT_VERSION__,
126125
}),
127126
computed: {

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ describe('DetailsInfo', () => {
2626
const application = new Application(applications[0]);
2727
const instance = application.instances[0];
2828
instance.hasEndpoint = () => true;
29-
// Use AxiosResponse type for the mock
30-
// eslint-disable-next-line @typescript-eslint/no-var-requires
3129

3230
instance.fetchInfo = async () => ({
3331
data: {

0 commit comments

Comments
 (0)