Skip to content

Commit 41e9cfc

Browse files
authored
fix(#2324): Shutdown button calls restart endpoint (#2337)
closes #2324
1 parent eeb0465 commit 41e9cfc

File tree

9 files changed

+139
-70
lines changed

9 files changed

+139
-70
lines changed

spring-boot-admin-server-ui/.jest/jest.setup.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import '@testing-library/jest-dom';
22

33
import { server } from '@/mocks/server';
44

5+
afterEach(() => {
6+
document.body.innerHTML = '';
7+
});
8+
59
beforeAll(async () => {
610
server.listen();
711

spring-boot-admin-server-ui/src/main/frontend/plugins/modal/Modal.vue

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<template>
22
<TransitionRoot
33
ref="root"
4-
appear
54
:show="isOpen"
5+
appear
66
as="template"
77
@close="closeModal"
88
>
@@ -63,10 +63,9 @@ import {
6363
TransitionChild,
6464
TransitionRoot,
6565
} from '@headlessui/vue';
66-
import { defineComponent, ref, render } from 'vue';
66+
import { defineComponent, ref } from 'vue';
6767
6868
import eventBus from '@/plugins/modal/bus';
69-
import { removeElement } from '@/plugins/modal/helpers';
7069
7170
export default defineComponent({
7271
components: {
@@ -97,12 +96,6 @@ export default defineComponent({
9796
methods: {
9897
closeModal() {
9998
this.isOpen = false;
100-
101-
setTimeout(() => {
102-
const wrapper = this.$refs.root;
103-
render(null, wrapper);
104-
removeElement(wrapper);
105-
}, 150);
10699
},
107100
},
108101
});

spring-boot-admin-server-ui/src/main/frontend/plugins/modal/api.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,35 @@ export const useModal = (globalProps = {}) => {
2222
};
2323

2424
const propsData = Object.assign({}, defaultProps, globalProps, options);
25-
createComponent(Modal, propsData, document.body, slots);
25+
return createComponent(Modal, propsData, document.body, slots);
2626
},
2727
async confirm(title, body) {
28-
this.open(
28+
let bodyFn = () =>
29+
h(
30+
'span',
31+
{
32+
innerHTML: body,
33+
},
34+
[]
35+
);
36+
37+
const { vNode, destroy } = this.open(
2938
{ title },
3039
{
3140
buttons: () =>
3241
h(ConfirmButtons, {
3342
labelOk: t('term.ok'),
3443
labelCancel: t('term.cancel'),
3544
}),
36-
body: () => h('span', { innerHTML: body }),
45+
body: bodyFn,
3746
}
3847
);
3948

4049
return new Promise((resolve) => {
41-
eventBus.on('sba-modal-close', resolve);
50+
eventBus.on('sba-modal-close', (result) => {
51+
destroy();
52+
resolve(result);
53+
});
4254
});
4355
},
4456
};

spring-boot-admin-server-ui/src/main/frontend/plugins/modal/helpers.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,22 @@ export function removeElement(el) {
99
}
1010

1111
export function createComponent(component, props, parentContainer, slots = {}) {
12-
const vNode = h(component, props, slots);
12+
let vNode = h(component, props, slots);
13+
1314
let container = parentContainer.querySelector('.sba-modal--wrapper');
1415
container = container || document.createElement('div');
1516
container.classList.add('sba-modal--wrapper');
1617
parentContainer.appendChild(container);
1718
render(vNode, container);
1819

19-
return vNode.component;
20+
const destroy = () => {
21+
if (container) render(null, container);
22+
container = null;
23+
vNode = null;
24+
};
25+
26+
return {
27+
vNode,
28+
destroy,
29+
};
2030
}

spring-boot-admin-server-ui/src/main/frontend/test-utils.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ export const render = (testComponent, options) => {
5555
},
5656
options
5757
);
58-
return tlRender(testComponent, renderOptions);
58+
let utils = tlRender(testComponent, renderOptions);
59+
return { ...utils };
5960
};

spring-boot-admin-server-ui/src/main/frontend/views/applications/applications-list-item.spec.js

Lines changed: 91 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,76 +14,123 @@
1414
* limitations under the License.
1515
*/
1616
import userEvent from '@testing-library/user-event';
17-
import { screen, waitFor } from '@testing-library/vue';
17+
import { screen, waitFor, within } from '@testing-library/vue';
1818
import { cloneDeep } from 'lodash-es';
1919

20-
import { applications } from '../../mocks/applications/data';
2120
import Application from '../../services/application';
22-
import { render } from '../../test-utils';
2321
import ApplicationListItem from './applications-list-item';
2422

25-
describe('application-list-item.vue', () => {
26-
let application;
23+
import { applications } from '@/mocks/applications/data';
24+
import { render } from '@/test-utils';
2725

28-
beforeEach(() => {
29-
application = cloneDeep(applications[0]);
26+
async function clickConfirmModal() {
27+
await waitFor(() => {
28+
expect(screen.getByRole('dialog')).toBeInTheDocument();
3029
});
3130

32-
it('does not show shutdown button when shutdown endpoint is missing', () => {
33-
application.instances[0].endpoints = [];
31+
const buttonOK = screen.queryByRole('button', { name: 'term.ok' });
32+
await userEvent.click(buttonOK);
33+
}
3434

35-
render(ApplicationListItem, {
36-
props: { application: new Application(application) },
35+
describe('application-list-item.vue', () => {
36+
describe('unregister', () => {
37+
it('on instance', async () => {
38+
let application = new Application(cloneDeep(applications[0]));
39+
const { emitted } = render(ApplicationListItem, {
40+
props: { application, isExpanded: true },
41+
});
42+
43+
const htmlElement = await screen.findByTestId(
44+
application.instances[0].id
45+
);
46+
const element = within(htmlElement).queryByTitle(
47+
'applications.actions.unregister'
48+
);
49+
await userEvent.click(element);
50+
51+
await clickConfirmModal();
52+
53+
await waitFor(() => expect(emitted('unregister')).toBeDefined());
3754
});
3855

39-
const shutdownButton = screen.queryByTitle('shutdown');
40-
expect(shutdownButton).toBeNull();
41-
});
56+
it('on application', async () => {
57+
const { emitted } = render(ApplicationListItem, {
58+
props: { application: new Application(cloneDeep(applications[0])) },
59+
});
4260

43-
it('should call shutdown endpoint when modal is confirmed', async () => {
44-
const { emitted } = render(ApplicationListItem, {
45-
props: { application: new Application(application) },
46-
});
61+
const element = screen.queryByTitle('applications.actions.unregister');
62+
await userEvent.click(element);
4763

48-
const element = screen.queryByTitle('shutdown');
49-
await userEvent.click(element);
64+
await clickConfirmModal();
5065

51-
await waitFor(() => {
52-
screen.findByRole('dialog');
66+
await waitFor(() => expect(emitted('unregister')).toBeDefined());
5367
});
68+
});
5469

55-
const buttonOK = screen.queryByRole('button', { name: 'OK' });
56-
await userEvent.click(buttonOK);
70+
describe('restart', () => {
71+
it('on instance', async () => {
72+
let application = new Application(cloneDeep(applications[0]));
73+
const { emitted } = render(ApplicationListItem, {
74+
props: { application, isExpanded: true },
75+
});
5776

58-
expect(emitted()).toBeDefined();
59-
});
77+
const htmlElement = await screen.findByTestId(
78+
application.instances[0].id
79+
);
80+
const element = within(htmlElement).queryByTitle(
81+
'applications.actions.restart'
82+
);
83+
await userEvent.click(element);
6084

61-
it('does not show restart button when restart endpoint is missing', () => {
62-
application.instances[0].endpoints = [];
85+
await clickConfirmModal();
6386

64-
render(ApplicationListItem, {
65-
props: { application: new Application(application) },
87+
await waitFor(() => expect(emitted('restart')).toBeDefined());
6688
});
6789

68-
const shutdownButton = screen.queryByTitle('applications.actions.restart');
69-
expect(shutdownButton).toBeNull();
70-
});
90+
it('on application', async () => {
91+
const { emitted } = render(ApplicationListItem, {
92+
props: { application: new Application(cloneDeep(applications[0])) },
93+
});
94+
95+
const element = screen.queryByTitle('applications.actions.restart');
96+
await userEvent.click(element);
97+
98+
await clickConfirmModal();
7199

72-
it('should call restart endpoint when modal is confirmed', async () => {
73-
const { emitted } = render(ApplicationListItem, {
74-
props: { application: new Application(application) },
100+
await waitFor(() => expect(emitted('restart')).toBeDefined());
75101
});
102+
});
103+
104+
describe('shutdown', () => {
105+
it('on application', async () => {
106+
const { emitted } = render(ApplicationListItem, {
107+
props: { application: new Application(cloneDeep(applications[0])) },
108+
});
109+
110+
const element = await screen.findByTitle('applications.actions.shutdown');
111+
await userEvent.click(element);
112+
113+
await clickConfirmModal();
76114

77-
const element = screen.queryByTitle('applications.actions.restart');
78-
await userEvent.click(element);
79-
await waitFor(() => {
80-
screen.findByRole('dialog');
115+
expect(emitted('shutdown')).toBeDefined();
81116
});
82117

83-
const buttonOK = screen.queryByRole('button', { name: 'term.ok' });
84-
await userEvent.click(buttonOK);
118+
it('on instance', async () => {
119+
let application = new Application(cloneDeep(applications[0]));
120+
const { emitted } = render(ApplicationListItem, {
121+
props: { application, isExpanded: true },
122+
});
85123

86-
let emitted1 = emitted();
87-
expect(emitted1.restart).toBeDefined();
124+
let htmlElement = await screen.findByTestId(application.instances[0].id);
125+
126+
const element = await within(htmlElement).findByTitle(
127+
'applications.actions.shutdown'
128+
);
129+
await userEvent.click(element);
130+
131+
await clickConfirmModal();
132+
133+
expect(emitted('shutdown')).toBeDefined();
134+
});
88135
});
89136
});

spring-boot-admin-server-ui/src/main/frontend/views/applications/applications-list-item.vue

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
<div
1919
:id="application.name"
2020
v-on-clickaway="(event) => $emit('deselect', event, application.name)"
21-
class="application-list-item"
2221
:class="{ 'is-active': isExpanded }"
22+
class="application-list-item"
2323
@click="$emit('select', application.name)"
2424
>
2525
<header
26-
class="application-list-item__header"
2726
:class="headerClass"
27+
class="application-list-item__header"
2828
v-on="$attrs"
2929
>
3030
<ApplicationsListItemSummary
@@ -38,15 +38,15 @@
3838
/>
3939
<div>
4040
<ApplicationListItemAction
41-
:item="application"
4241
:has-active-notification-filter="
4342
hasActiveNotificationFilter(application)
4443
"
4544
:has-notification-filters-support="hasNotificationFiltersSupport"
46-
@filter-settings="toggleFilterSettings"
45+
:item="application"
4746
@restart="confirmRestartApplication"
4847
@shutdown="confirmShutdownApplication"
4948
@unregister="confirmUnregisterApplication"
49+
@filter-settings="toggleFilterSettings"
5050
/>
5151
</div>
5252
</header>
@@ -55,16 +55,16 @@
5555
<instances-list :instances="application.instances">
5656
<template #actions="{ instance }">
5757
<ApplicationListItemAction
58-
class="hidden md:flex"
59-
:item="instance"
6058
:has-active-notification-filter="
6159
hasActiveNotificationFilter(instance)
6260
"
6361
:has-notification-filters-support="hasNotificationFiltersSupport"
64-
@filter-settings="toggleFilterSettings"
62+
:item="instance"
63+
class="hidden md:flex"
6564
@restart="confirmRestartInstance"
6665
@shutdown="confirmShutdownInstance"
6766
@unregister="confirmUnregisterInstance"
67+
@filter-settings="toggleFilterSettings"
6868
/>
6969
</template>
7070
</instances-list>
@@ -135,7 +135,7 @@ export default {
135135
this.$t('applications.shutdown', { name: application.name })
136136
);
137137
if (isConfirmed) {
138-
this.$emit('restart', application);
138+
this.$emit('shutdown', application);
139139
}
140140
},
141141
async confirmUnregisterApplication(application) {
@@ -180,7 +180,7 @@ export default {
180180
this.$t('instances.shutdown', { name: instance.id })
181181
);
182182
if (isConfirmed) {
183-
this.$emit('restart', instance);
183+
this.$emit('shutdown', instance);
184184
}
185185
},
186186
},

spring-boot-admin-server-ui/src/main/frontend/views/applications/i18n.en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
},
3838
"instances": {
3939
"shutdown": "Shutdown instance <code>{name}</code>?",
40-
"shutdown_successful": "Successfully restarted instances {name}.",
40+
"shutdown_successful": "Successfully shutdown instances {name}.",
41+
"shutdown_failed": "Failed to shutdown instances {name}.",
4142
"restart": "Restart instance <code>{name}</code>?",
4243
"restarted": "Successfully restarted instance {name}.",
4344
"unregister": "Deregister instance <code>{name}</code>?",

spring-boot-admin-server-ui/src/main/frontend/views/applications/instances-list.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<li
1919
v-for="instance in instances"
2020
:key="instance.id"
21+
:data-testid="instance.id"
2122
class="flex items-center hover:bg-gray-100 p-2"
2223
@click.stop="showDetails(instance)"
2324
>

0 commit comments

Comments
 (0)