Skip to content

Commit 3206e6c

Browse files
authored
Fixes #5003: suppress toast on error (#5044)
1 parent 6363873 commit 3206e6c

9 files changed

Lines changed: 539 additions & 163 deletions

File tree

spring-boot-admin-server-ui/package-lock.json

Lines changed: 221 additions & 134 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spring-boot-admin-server-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"@vue/eslint-config-typescript": "^14.0.0",
9090
"@vue/test-utils": "2.4.6",
9191
"autoprefixer": "10.4.27",
92+
"axios-mock-adapter": "^2.1.0",
9293
"babel-loader": "10.1.1",
9394
"eslint": "^10.0.0",
9495
"eslint-config-prettier": "^10.0.0",

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

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AxiosError } from 'axios';
12
import { describe, expect, test, vi } from 'vitest';
23

34
import Instance from '@/services/instance';
@@ -30,6 +31,8 @@ describe('Instance', () => {
3031

3132
const instance = new Instance({
3233
id: 'id',
34+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
35+
// @ts-expect-error
3336
registration: {
3437
metadata: {
3538
['hide-url']: metadataHideUrl,
@@ -40,4 +43,142 @@ describe('Instance', () => {
4043
expect(instance.showUrl()).toEqual(expectUrlToBeShownOnUI);
4144
},
4245
);
46+
47+
describe('fetchMetric', () => {
48+
const instance = new Instance({
49+
id: 'test-id',
50+
registration: {
51+
name: 'test',
52+
healthUrl: '',
53+
source: '',
54+
},
55+
availableMetrics: ['test.metric', 'cache.size', 'cache.gets'],
56+
});
57+
test('should pass suppressToast option to axios config', async () => {
58+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
59+
// @ts-expect-error
60+
// Spy on axios.get
61+
const axiosGetSpy = vi.spyOn(instance.axios, 'get');
62+
63+
// Mock the axios request
64+
axiosGetSpy.mockResolvedValue({
65+
data: {
66+
measurements: [{ value: 42 }],
67+
},
68+
});
69+
70+
await instance.fetchMetric(
71+
'test.metric',
72+
{ tag: 'value' },
73+
{
74+
suppressToast: true,
75+
},
76+
);
77+
78+
// Verify suppressToast was passed in config
79+
expect(axiosGetSpy).toHaveBeenCalledWith(
80+
expect.stringContaining('actuator/metrics/test.metric'),
81+
expect.objectContaining({
82+
suppressToast: true,
83+
}),
84+
);
85+
});
86+
87+
test('should work without options parameter for backward compatibility', async () => {
88+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
89+
// @ts-expect-error
90+
const axiosGetSpy = vi.spyOn(instance.axios, 'get');
91+
92+
axiosGetSpy.mockResolvedValue({
93+
data: {
94+
measurements: [{ value: 42 }],
95+
},
96+
});
97+
98+
await instance.fetchMetric('test.metric', { tag: 'value' });
99+
100+
// Verify it was called without suppressToast
101+
expect(axiosGetSpy).toHaveBeenCalledWith(
102+
expect.stringContaining('actuator/metrics/test.metric'),
103+
expect.objectContaining({
104+
suppressToast: undefined,
105+
}),
106+
);
107+
});
108+
109+
test('should pass suppressToast=false when explicitly set to false', async () => {
110+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
111+
// @ts-expect-error
112+
const axiosGetSpy = vi.spyOn(instance.axios, 'get');
113+
114+
axiosGetSpy.mockResolvedValue({
115+
data: {
116+
measurements: [{ value: 42 }],
117+
},
118+
});
119+
120+
await instance.fetchMetric(
121+
'test.metric',
122+
{ tag: 'value' },
123+
{
124+
suppressToast: false,
125+
},
126+
);
127+
128+
expect(axiosGetSpy).toHaveBeenCalledWith(
129+
expect.stringContaining('actuator/metrics/test.metric'),
130+
expect.objectContaining({
131+
suppressToast: false,
132+
}),
133+
);
134+
});
135+
136+
test('should include tags in request parameters', async () => {
137+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
138+
// @ts-expect-error
139+
const axiosGetSpy = vi.spyOn(instance.axios, 'get');
140+
141+
axiosGetSpy.mockResolvedValue({
142+
data: {
143+
measurements: [{ value: 42 }],
144+
},
145+
});
146+
147+
await instance.fetchMetric('cache.gets', {
148+
name: 'my-cache',
149+
result: 'hit',
150+
});
151+
152+
const callArgs = axiosGetSpy.mock.calls[0];
153+
const params = callArgs[1]?.params as URLSearchParams;
154+
155+
expect(params).toBeInstanceOf(URLSearchParams);
156+
expect(params.getAll('tag')).toContain('name:my-cache');
157+
expect(params.getAll('tag')).toContain('result:hit');
158+
});
159+
160+
test('should pass suppressToast function to axios config', async () => {
161+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
162+
// @ts-expect-error
163+
const axiosGetSpy = vi.spyOn(instance.axios, 'get');
164+
165+
axiosGetSpy.mockResolvedValue({
166+
data: {
167+
measurements: [{ value: 42 }],
168+
},
169+
});
170+
171+
const suppressFn = (err: AxiosError) => err.response?.status === 404;
172+
await instance.fetchMetric(
173+
'cache.size',
174+
{},
175+
{ suppressToast: suppressFn },
176+
);
177+
178+
expect(axiosGetSpy).toHaveBeenCalledWith(
179+
expect.any(String),
180+
expect.objectContaining({ suppressToast: suppressFn }),
181+
);
182+
});
183+
});
43184
});

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { AxiosInstance } from 'axios';
16+
import { AxiosError, AxiosInstance } from 'axios';
1717
import saveAs from 'file-saver';
1818
import { Observable, concat, from, ignoreElements } from 'rxjs';
1919

@@ -29,6 +29,17 @@ import { useSbaConfig } from '@/sba-config';
2929
import { actuatorMimeTypes } from '@/services/spring-mime-types';
3030
import { transformToJSON } from '@/utils/transformToJSON';
3131

32+
// Extend AxiosRequestConfig to allow suppressToast
33+
declare module 'axios' {
34+
interface AxiosRequestConfig {
35+
suppressToast?: boolean | ((error: AxiosError) => boolean);
36+
}
37+
}
38+
39+
export type FetchMetricOptions = {
40+
suppressToast?: boolean | ((error: AxiosError) => boolean);
41+
};
42+
3243
const isInstanceActuatorRequest = (url: string) =>
3344
url.match(/^instances[/][^/]+[/]actuator([/].*)?$/);
3445

@@ -168,7 +179,11 @@ class Instance {
168179
return response;
169180
}
170181

171-
async fetchMetric(metric: string, tags: Record<string, any>) {
182+
async fetchMetric(
183+
metric: string,
184+
tags: Record<string, any>,
185+
options?: FetchMetricOptions,
186+
) {
172187
if (this.availableMetrics.length === 0) {
173188
try {
174189
await this.fetchMetrics();
@@ -204,6 +219,7 @@ class Instance {
204219
}
205220
return this.axios.get(uri`actuator/metrics/${metric}`, {
206221
params,
222+
suppressToast: options?.suppressToast,
207223
});
208224
}
209225

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,36 @@ import { afterAll, afterEach, beforeAll, vi } from 'vitest';
66
import { server } from '@/mocks/server';
77
import sbaConfig from '@/sba-config';
88

9+
// Setup localStorage mock
10+
const localStorageMock = (() => {
11+
let store: Record<string, string> = {};
12+
13+
return {
14+
get length(): number {
15+
return Object.keys(store).length;
16+
},
17+
getItem: (key: string) => store[key] || null,
18+
setItem: (key: string, value: string) => {
19+
store[key] = value.toString();
20+
},
21+
removeItem: (key: string) => {
22+
delete store[key];
23+
},
24+
clear: () => {
25+
store = {};
26+
},
27+
};
28+
})();
29+
30+
Object.defineProperty(window, 'localStorage', {
31+
value: localStorageMock,
32+
});
33+
34+
// Setup globalThis.errorSpy for toast notifications
35+
if (!globalThis.errorSpy) {
36+
globalThis.errorSpy = vi.fn();
37+
}
38+
939
global.IntersectionObserver = vi.fn().mockImplementation(function () {
1040
return {
1141
observe: vi.fn(),
@@ -24,10 +54,11 @@ global.matchMedia = vi.fn().mockReturnValue({
2454
addEventListener: vi.fn(),
2555
removeEventListener: vi.fn(),
2656
});
27-
global.EventSource = class {
28-
constructor() {}
29-
close() {}
30-
};
57+
global.EventSource = vi.fn().mockImplementation(function () {
58+
return {
59+
close: vi.fn(),
60+
};
61+
}) as unknown as typeof EventSource;
3162

3263
global.SBA = sbaConfig;
3364

@@ -38,5 +69,6 @@ afterEach(() => server.resetHandlers());
3869
// runs a cleanup after each test case (e.g. clearing jsdom)
3970
afterEach(() => {
4071
vi.clearAllMocks();
72+
localStorage.clear();
4173
cleanup();
4274
});

0 commit comments

Comments
 (0)