Skip to content

Commit e4944d2

Browse files
authored
feat: enhance UX with improved icon transitions and button interactions (#5344)
1 parent 8939deb commit e4944d2

30 files changed

Lines changed: 351 additions & 168 deletions

File tree

spring-boot-admin-server-ui/src/main/frontend/components/sba-accordion.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
{
4141
'-rotate-90': !open,
4242
},
43-
'mr-2 transition-[transform]',
43+
'mr-2 transition-transform',
4444
)
4545
"
4646
/>
@@ -90,3 +90,11 @@ const handleTitleClick = () => {
9090
open.value = !open.value;
9191
};
9292
</script>
93+
94+
<style scoped>
95+
@reference "../index.css";
96+
97+
:deep(header button) {
98+
@apply cursor-pointer;
99+
}
100+
</style>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import userEvent from '@testing-library/user-event';
2+
import { screen } from '@testing-library/vue';
3+
import { describe, expect, it } from 'vitest';
4+
import { defineComponent } from 'vue';
5+
6+
import SbaButton from './sba-button.vue';
7+
8+
import { render } from '@/test-utils';
9+
10+
describe('SbaButton', () => {
11+
it('renders as a button element by default', () => {
12+
render(SbaButton, { slots: { default: 'Click me' } });
13+
expect(
14+
screen.getByRole('button', { name: 'Click me' }),
15+
).toBeInTheDocument();
16+
});
17+
18+
it('renders slot content', () => {
19+
render(SbaButton, { slots: { default: 'Submit' } });
20+
expect(screen.getByText('Submit')).toBeVisible();
21+
});
22+
23+
it('renders as an anchor element when as="a"', () => {
24+
render(SbaButton, {
25+
props: { as: 'a', href: '#' },
26+
slots: { default: 'Link' },
27+
});
28+
expect(screen.getByRole('link', { name: 'Link' })).toBeInTheDocument();
29+
});
30+
31+
it('sets href on anchor element', () => {
32+
render(SbaButton, {
33+
props: { as: 'a', href: 'https://example.com' },
34+
slots: { default: 'Link' },
35+
});
36+
expect(screen.getByRole('link', { name: 'Link' })).toHaveAttribute(
37+
'href',
38+
'https://example.com',
39+
);
40+
});
41+
42+
it('sets title attribute', () => {
43+
render(SbaButton, {
44+
props: { title: 'My tooltip' },
45+
slots: { default: 'Btn' },
46+
});
47+
expect(screen.getByRole('button', { name: 'Btn' })).toHaveAttribute(
48+
'title',
49+
'My tooltip',
50+
);
51+
});
52+
53+
it('is disabled when disabled prop is true', () => {
54+
render(SbaButton, {
55+
props: { disabled: true },
56+
slots: { default: 'Disabled' },
57+
});
58+
expect(screen.getByRole('button', { name: 'Disabled' })).toBeDisabled();
59+
});
60+
61+
it('is not disabled by default', () => {
62+
render(SbaButton, { slots: { default: 'Active' } });
63+
expect(screen.getByRole('button', { name: 'Active' })).not.toBeDisabled();
64+
});
65+
66+
it('emits click event when button is clicked', async () => {
67+
const { emitted } = render(SbaButton, { slots: { default: 'Click' } });
68+
await userEvent.click(screen.getByRole('button', { name: 'Click' }));
69+
expect(emitted().click).toHaveLength(1);
70+
});
71+
72+
it('does not emit click event when rendered as anchor', async () => {
73+
const { emitted } = render(SbaButton, {
74+
props: { as: 'a', href: '#' },
75+
slots: { default: 'Link' },
76+
});
77+
await userEvent.click(screen.getByRole('link', { name: 'Link' }));
78+
expect(emitted().click).toBeUndefined();
79+
});
80+
81+
it('accepts a Vue component as the as prop and emits click', async () => {
82+
const StubComponent = defineComponent({
83+
template: '<button v-bind="$attrs"><slot /></button>',
84+
});
85+
const { emitted } = render(SbaButton, {
86+
props: { as: StubComponent },
87+
slots: { default: 'Component' },
88+
});
89+
await userEvent.click(screen.getByRole('button', { name: 'Component' }));
90+
expect(emitted().click).toHaveLength(1);
91+
});
92+
93+
it('passes attrs through to a component passed as the as prop', () => {
94+
const StubComponent = defineComponent({
95+
template: '<span v-bind="$attrs"><slot /></span>',
96+
});
97+
render(SbaButton, {
98+
props: { as: StubComponent, primary: true },
99+
slots: { default: 'Component' },
100+
});
101+
expect(screen.getByText('Component')).toHaveClass('is-primary');
102+
});
103+
104+
it('applies is-primary class when primary prop is true', () => {
105+
render(SbaButton, {
106+
props: { primary: true },
107+
slots: { default: 'Primary' },
108+
});
109+
expect(screen.getByRole('button', { name: 'Primary' })).toHaveClass(
110+
'is-primary',
111+
);
112+
});
113+
114+
it('does not apply is-primary class by default', () => {
115+
render(SbaButton, { slots: { default: 'Default' } });
116+
expect(screen.getByRole('button', { name: 'Default' })).not.toHaveClass(
117+
'is-primary',
118+
);
119+
});
120+
121+
it.each([
122+
['2xs', 'px-1.5'],
123+
['xs', 'px-2'],
124+
['sm', 'px-3'],
125+
['base', 'px-4'],
126+
])('applies correct padding class for size="%s"', (size, expectedClass) => {
127+
render(SbaButton, { props: { size }, slots: { default: 'Btn' } });
128+
expect(screen.getByRole('button', { name: 'Btn' })).toHaveClass(
129+
expectedClass,
130+
);
131+
});
132+
});

spring-boot-admin-server-ui/src/main/frontend/components/sba-button.vue

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<component
33
:is="as"
4-
class="btn relative items-center"
4+
class="btn relative items-center cursor-pointer"
55
v-bind="componentAttrs"
66
@click="handleClick"
77
>
@@ -19,11 +19,8 @@ const props = defineProps({
1919
default: '',
2020
},
2121
as: {
22-
type: String,
22+
type: [String, Object, Function],
2323
default: 'button',
24-
validator(value) {
25-
return ['a', 'button'].includes(value);
26-
},
2724
},
2825
href: {
2926
type: String,
@@ -49,7 +46,7 @@ const attrs = useAttrs();
4946
5047
const cssClasses = computed(() => {
5148
return {
52-
'px-1 py-0 text-xs': props.size === '2xs',
49+
'px-1.5 py-0.5 text-xs': props.size === '2xs',
5350
'px-2 py-2 text-xs': props.size === 'xs',
5451
'px-3 py-2': props.size === 'sm',
5552
'px-4 py-3': props.size === 'base',
@@ -76,20 +73,19 @@ const componentAttrs = computed(() => {
7673
return {
7774
...common,
7875
disabled: props.disabled === true,
79-
type: props.type,
76+
type: 'button',
8077
};
8178
}
82-
return {};
79+
return common;
8380
});
8481
8582
const emit = defineEmits(['click']);
8683
const handleClick = (event) => {
87-
if (props.as === 'button') {
88-
emit('click', event);
89-
}
9084
if (props.as === 'a') {
9185
event.stopPropagation();
86+
return;
9287
}
88+
emit('click', event);
9389
};
9490
</script>
9591

spring-boot-admin-server-ui/src/main/frontend/index.css

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,6 @@ th {
7777
@apply border-b border-gray-200;
7878
}
7979

80-
.-rotate-90 {
81-
--tw-rotate: -90deg;
82-
transform: rotate(var(--tw-rotate));
83-
}
84-
85-
.rotate-90 {
86-
--tw-rotate: 90deg;
87-
transform: rotate(var(--tw-rotate));
88-
}
89-
9080
table.table-wide td {
9181
@apply px-2 py-1.5;
9282
}

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

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,50 @@
22
<sba-button-group class="application-list-item__header__actions text-right">
33
<router-link v-slot="{ navigate }" :to="journalLink" custom>
44
<sba-button
5-
:title="$t('applications.actions.journal')"
5+
:size="size === 'xs' ? '2xs' : undefined"
6+
:title="t('applications.actions.journal')"
67
@click.stop="navigate"
78
>
8-
<font-awesome-icon :icon="faHistory" />
9+
<font-awesome-icon :icon="faScroll" :size="size" />
910
</sba-button>
1011
</router-link>
1112
<sba-button
1213
v-if="hasNotificationFiltersSupport"
1314
:id="`nf-settings-${item.name || item.id}`"
15+
:size="size === 'xs' ? '2xs' : undefined"
1416
:title="$t('applications.actions.notification_filters')"
1517
@click.stop="$emit('filter-settings', item)"
1618
>
1719
<font-awesome-icon
20+
:size="size"
1821
:icon="hasActiveNotificationFilter ? faBellSlash : faBell"
1922
/>
2023
</sba-button>
2124
<sba-button
2225
v-if="item.isUnregisterable"
2326
class="btn-unregister"
24-
:title="$t('applications.actions.unregister')"
27+
:size="size === 'xs' ? '2xs' : undefined"
28+
:title="t('applications.actions.unregister')"
2529
@click.stop="actionHandler.unregister(item)"
2630
>
27-
<font-awesome-icon :icon="faTrash" />
31+
<font-awesome-icon :size="size" :icon="faTrash" />
2832
</sba-button>
2933
<sba-button
3034
v-if="item.hasEndpoint('restart')"
31-
:title="$t('applications.actions.restart')"
35+
:size="size === 'xs' ? '2xs' : undefined"
36+
:title="t('applications.actions.restart')"
3237
@click.stop="actionHandler.restart(item)"
3338
>
34-
<font-awesome-icon :icon="faUndoAlt" />
39+
<font-awesome-icon :size="size" :icon="faUndoAlt" />
3540
</sba-button>
3641
<sba-button
3742
v-if="item.hasEndpoint('shutdown')"
38-
:title="$t('applications.actions.shutdown')"
43+
:size="size === 'xs' ? '2xs' : undefined"
44+
:title="t('applications.actions.shutdown')"
3945
class="is-danger btn-shutdown"
4046
@click.stop="actionHandler.shutdown(item)"
4147
>
42-
<font-awesome-icon :icon="faPowerOff" />
48+
<font-awesome-icon :size="size" :icon="faPowerOff" />
4349
</sba-button>
4450
</sba-button-group>
4551
</template>
@@ -48,16 +54,18 @@
4854
import {
4955
faBell,
5056
faBellSlash,
51-
faHistory,
5257
faPowerOff,
58+
faScroll,
5359
faTrash,
5460
faUndoAlt,
5561
} from '@fortawesome/free-solid-svg-icons';
5662
import { useNotificationCenter } from '@stekoe/vue-toast-notificationcenter';
57-
import { inject } from 'vue';
63+
import { PropType, inject } from 'vue';
5864
import { useI18n } from 'vue-i18n';
5965
import { RouteLocationNamedRaw } from 'vue-router';
6066
67+
import SbaButtonGroup from '@/components/sba-button-group.vue';
68+
6169
import Application from '@/services/application';
6270
import Instance from '@/services/instance';
6371
import {
@@ -83,6 +91,10 @@ const props = defineProps({
8391
type: Boolean,
8492
default: false,
8593
},
94+
size: {
95+
type: String as PropType<'xs' | 'sm'>,
96+
default: 'sm',
97+
},
8698
});
8799
88100
defineEmits(['filter-settings']);
@@ -110,9 +122,4 @@ if (props.item instanceof Application) {
110122
.application-list-item__header__actions {
111123
@apply hidden lg:inline-flex p-1 bg-black/5 rounded-lg;
112124
}
113-
114-
.btn-shutdown,
115-
.btn-unregister {
116-
@apply ml-1;
117-
}
118125
</style>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
v-for="instance in instances"
2121
:key="instance.id"
2222
:data-testid="instance.id"
23-
class="flex p-2 pr-4 hover:bg-gray-100 gap-2 odd:bg-gray-50 items-center"
23+
class="flex p-2 pr-4 hover:bg-gray-100 hover:cursor-pointer gap-2 odd:bg-gray-50 items-center"
2424
@click.stop="showDetails(instance)"
2525
>
2626
<div class="pt-1 md:w-16 text-center">

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
},
4141
"instances": {
4242
"shutdown": "Möchten Sie die Instanz <code>{name}</code> herunterfahren?",
43+
"open_details": "Instanzdetails öffnen",
4344
"restart": "Möchten Sie die Instanz <code>{name}</code> neu starten?",
4445
"restarted": "Die Instanz {name} wurde neu gestartet.",
4546
"unregister": "Möchten Sie die Instanz <code>{name}</code> deregistrieren?",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"shutdown": "Shutdown instance <code>{name}</code>?",
4747
"shutdown_successful": "Successfully shutdown instances {name}.",
4848
"shutdown_failed": "Failed to shutdown instances {name}.",
49+
"open_details": "Open instance details",
4950
"restart": "Restart instance <code>{name}</code>?",
5051
"restarted": "Successfully restarted instance {name}.",
5152
"unregister": "Deregister instance <code>{name}</code>?",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
},
2020
"instances": {
2121
"shutdown": "Detener instancia <code>{name}</code>?",
22+
"open_details": "Abrir detalles de la instancia",
2223
"restart": "Reinciar instancia <code>{name}</code>?",
2324
"restarted": "Instancia reiniciada exitosamente"
2425
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
},
2020
"instances": {
2121
"shutdown": "Shutdown instance <code>{name}</code>?",
22+
"open_details": "Ouvrir les détails de l'instance",
2223
"restart": "Restart instance <code>{name}</code>?",
2324
"restarted": "Successfully restarted instance"
2425
}

0 commit comments

Comments
 (0)