Skip to content

Commit 2047f8a

Browse files
Harshit-0413SteKoe
andauthored
fix: Sidebar group should toggle open/close on click (#5263) (#5267)
Co-authored-by: Stephan Köninger <stephan.koeninger@codecentric.de>
1 parent 164f6bc commit 2047f8a

3 files changed

Lines changed: 291 additions & 32 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ export type Registration = {
515515
metadata?: { [key: string]: string };
516516
};
517517

518-
type StatusInfo = {
518+
export type StatusInfo = {
519519
status:
520520
| 'UNKNOWN'
521521
| 'OUT_OF_SERVICE'
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import userEvent from '@testing-library/user-event';
2+
import { render, screen, waitFor, within } from '@testing-library/vue';
3+
import { describe, expect, it } from 'vitest';
4+
import { defineComponent, h, markRaw } from 'vue';
5+
import { createI18n } from 'vue-i18n';
6+
import { createMemoryHistory, createRouter } from 'vue-router';
7+
8+
import Sidebar from './sidebar.vue';
9+
10+
import Application from '@/services/application';
11+
import Instance from '@/services/instance';
12+
import { VIEW_GROUP } from '@/views/ViewGroup';
13+
14+
const TestComponent = defineComponent({
15+
render() {
16+
return h('div');
17+
},
18+
});
19+
20+
const RouterLinkStub = defineComponent({
21+
props: {
22+
to: {
23+
type: [String, Object],
24+
required: true,
25+
},
26+
},
27+
setup(_, { slots, attrs }) {
28+
return () => h('a', attrs, slots.default?.());
29+
},
30+
});
31+
32+
describe('Sidebar', () => {
33+
it('sets openGroup to the currently navigated group', async () => {
34+
const { router, views } = await renderSidebar({
35+
initialRouteName: 'instances/web/health',
36+
});
37+
38+
await waitFor(() => {
39+
expect(screen.getByText('Overview')).toBeInTheDocument();
40+
expect(screen.getByText('Health')).toBeInTheDocument();
41+
});
42+
expect(screen.queryByText('Environment')).not.toBeInTheDocument();
43+
expect(screen.queryByText('Config Props')).not.toBeInTheDocument();
44+
45+
await router.push({
46+
name: views.dataConfigpropsView.name,
47+
params: { instanceId: 'instance-1' },
48+
});
49+
50+
await waitFor(() => {
51+
expect(screen.getByText('Environment')).toBeInTheDocument();
52+
expect(screen.getByText('Config Props')).toBeInTheDocument();
53+
});
54+
expect(screen.queryByText('Overview')).not.toBeInTheDocument();
55+
expect(screen.queryByText('Health')).not.toBeInTheDocument();
56+
});
57+
58+
it('toggles the currently navigated group closed by button click', async () => {
59+
const user = userEvent.setup();
60+
61+
const { container } = await renderSidebar({
62+
initialRouteName: 'instances/web/health',
63+
});
64+
65+
await waitFor(() => {
66+
expect(screen.getByText('Overview')).toBeInTheDocument();
67+
expect(screen.getByText('Health')).toBeInTheDocument();
68+
});
69+
70+
await user.click(getGroupToggleButton(container, VIEW_GROUP.WEB));
71+
72+
await waitFor(() => {
73+
expect(screen.queryByText('Overview')).not.toBeInTheDocument();
74+
expect(screen.queryByText('Health')).not.toBeInTheDocument();
75+
});
76+
});
77+
78+
it('opens another non-navigated group by button click', async () => {
79+
const user = userEvent.setup();
80+
81+
const { container } = await renderSidebar({
82+
initialRouteName: 'instances/web/health',
83+
});
84+
85+
await waitFor(() => {
86+
expect(screen.getByText('Overview')).toBeInTheDocument();
87+
expect(screen.getByText('Health')).toBeInTheDocument();
88+
});
89+
90+
await user.click(getGroupToggleButton(container, VIEW_GROUP.DATA));
91+
92+
await waitFor(() => {
93+
expect(screen.getByText('Environment')).toBeInTheDocument();
94+
expect(screen.getByText('Config Props')).toBeInTheDocument();
95+
});
96+
expect(screen.queryByText('Overview')).not.toBeInTheDocument();
97+
expect(screen.queryByText('Health')).not.toBeInTheDocument();
98+
});
99+
});
100+
101+
async function renderSidebar({
102+
initialRouteName,
103+
}: {
104+
initialRouteName: string;
105+
}) {
106+
const views = createViews();
107+
const router = createRouter({
108+
history: createMemoryHistory(),
109+
routes: [
110+
{
111+
path: '/instances/:instanceId/details',
112+
name: 'instances/details',
113+
component: TestComponent,
114+
},
115+
{
116+
path: '/instances/:instanceId/web/overview',
117+
name: views.webOverviewView.name,
118+
component: TestComponent,
119+
meta: { view: views.webOverviewView },
120+
},
121+
{
122+
path: '/instances/:instanceId/web/health',
123+
name: views.webHealthView.name,
124+
component: TestComponent,
125+
meta: { view: views.webHealthView },
126+
},
127+
{
128+
path: '/instances/:instanceId/data/env',
129+
name: views.dataEnvView.name,
130+
component: TestComponent,
131+
meta: { view: views.dataEnvView },
132+
},
133+
{
134+
path: '/instances/:instanceId/data/configprops',
135+
name: views.dataConfigpropsView.name,
136+
component: TestComponent,
137+
meta: { view: views.dataConfigpropsView },
138+
},
139+
],
140+
});
141+
142+
await router.push({
143+
name: initialRouteName,
144+
params: { instanceId: 'instance-1' },
145+
});
146+
await router.isReady();
147+
148+
return {
149+
router,
150+
views,
151+
...render(Sidebar, {
152+
props: {
153+
views: Object.values(views),
154+
instance: {
155+
id: 'instance-1',
156+
registration: { name: 'test-app' },
157+
statusInfo: { status: 'UP' },
158+
metadataParsed: {},
159+
} as Instance,
160+
application: {} as Application,
161+
},
162+
global: {
163+
plugins: [
164+
router,
165+
createI18n({
166+
locale: 'en',
167+
messages: { en: {} },
168+
legacy: false,
169+
fallbackWarn: false,
170+
missingWarn: false,
171+
}),
172+
],
173+
stubs: {
174+
RouterLink: RouterLinkStub,
175+
Divider: true,
176+
FontAwesomeIcon: true,
177+
},
178+
},
179+
}),
180+
};
181+
}
182+
183+
function createViews() {
184+
return {
185+
webOverviewView: createView({
186+
name: 'instances/web/overview',
187+
group: VIEW_GROUP.WEB,
188+
order: 10,
189+
label: 'sidebar.web.overview',
190+
handleText: 'Overview',
191+
}),
192+
webHealthView: createView({
193+
name: 'instances/web/health',
194+
group: VIEW_GROUP.WEB,
195+
order: 20,
196+
label: 'sidebar.web.health',
197+
handleText: 'Health',
198+
}),
199+
dataEnvView: createView({
200+
name: 'instances/data/env',
201+
group: VIEW_GROUP.DATA,
202+
order: 30,
203+
label: 'sidebar.data.env',
204+
handleText: 'Environment',
205+
}),
206+
dataConfigpropsView: createView({
207+
name: 'instances/data/configprops',
208+
group: VIEW_GROUP.DATA,
209+
order: 40,
210+
label: 'sidebar.data.configprops',
211+
handleText: 'Config Props',
212+
}),
213+
};
214+
}
215+
216+
function getGroupToggleButton(container: Element, groupId: string) {
217+
const group = container.querySelector(`[data-sba-group="${groupId}"]`);
218+
219+
if (!group) {
220+
throw new Error(`Group ${groupId} not found`);
221+
}
222+
223+
return within(group as HTMLElement).getByRole('button');
224+
}
225+
226+
function createView({
227+
name,
228+
group,
229+
order,
230+
label,
231+
handleText,
232+
}: {
233+
name: string;
234+
group: string;
235+
order: number;
236+
label: string;
237+
handleText: string;
238+
}): SbaView {
239+
return {
240+
id: name,
241+
name,
242+
parent: 'instances',
243+
handle: markRaw(
244+
defineComponent({
245+
render() {
246+
return h('span', handleText);
247+
},
248+
}),
249+
),
250+
order,
251+
component: markRaw(TestComponent),
252+
group,
253+
hasChildren: false,
254+
props: {},
255+
isEnabled: () => true,
256+
label,
257+
} as SbaView;
258+
}

0 commit comments

Comments
 (0)