Switch to Hilla for UI

This commit is contained in:
grimsi
2024-03-06 23:35:41 +01:00
parent 73457aad0b
commit e79dd7a6df
44 changed files with 12778 additions and 429 deletions
-32
View File
@@ -1,32 +0,0 @@
This directory is automatically generated by Vaadin and contains the pre-compiled
frontend files/resources for your project (frontend development bundle).
It should be added to Version Control System and committed, so that other developers
do not have to compile it again.
Frontend development bundle is automatically updated when needed:
- an npm/pnpm package is added with @NpmPackage or directly into package.json
- CSS, JavaScript or TypeScript files are added with @CssImport, @JsModule or @JavaScript
- Vaadin add-on with front-end customizations is added
- Custom theme imports/assets added into 'theme.json' file
- Exported web component is added.
If your project development needs a hot deployment of the frontend changes,
you can switch Flow to use Vite development server (default in Vaadin 23.3 and earlier versions):
- set `vaadin.frontend.hotdeploy=true` in `application.properties`
- configure `vaadin-maven-plugin`:
```
<configuration>
<frontendHotdeploy>true</frontendHotdeploy>
</configuration>
```
- configure `jetty-maven-plugin`:
```
<configuration>
<systemProperties>
<vaadin.frontend.hotdeploy>true</vaadin.frontend.hotdeploy>
</systemProperties>
</configuration>
```
Read more [about Vaadin development mode](https://vaadin.com/docs/next/configuration/development-mode/#pre-compiled-front-end-bundle-for-faster-start-up).
Binary file not shown.
@@ -1,90 +0,0 @@
package de.grimsi.gameyfin.layouts;
import com.flowingcode.vaadin.addons.fontawesome.FontAwesome;
import com.vaadin.flow.component.ClickEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.contextmenu.MenuItem;
import com.vaadin.flow.component.contextmenu.SubMenu;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.menubar.MenuBarVariant;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.spring.security.AuthenticationContext;
import de.grimsi.gameyfin.resources.PublicResources;
import de.grimsi.gameyfin.services.ThemeService;
import de.grimsi.gameyfin.setup.SetupService;
import de.grimsi.gameyfin.views.SetupView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.vaadin.firitin.util.style.LumoProps;
import static de.grimsi.gameyfin.users.util.Utils.isAdmin;
@JsModule("./scripts/prefers-color-scheme.js")
@CssImport("./styles/header.css")
public class MainLayout extends AppLayout {
public MainLayout(AuthenticationContext authContext,
@Autowired SetupService setupService,
@Autowired ThemeService themeService) {
if (!setupService.isSetupCompleted()) {
UI.getCurrent().navigate(SetupView.class);
UI.getCurrent().close();
}
UserDetails user = authContext.getAuthenticatedUser(UserDetails.class).get();
Image logo = new Image(PublicResources.GAMEYFIN_LOGO.path, "Gameyfin Logo");
logo.addClassName("header-logo");
Button toggleTheme = new Button(FontAwesome.Solid.CIRCLE_HALF_STROKE.create());
toggleTheme.addThemeVariants(ButtonVariant.LUMO_ICON);
toggleTheme.addClickListener(listener -> themeService.toggleTheme());
Avatar avatar = new Avatar(user.getUsername());
avatar.setAbbreviation(user.getUsername().substring(0, 2).toUpperCase());
avatar.setColorIndex(user.getUsername().chars().map(i -> i % 6).findFirst().getAsInt());
MenuBar menu = new MenuBar();
menu.addThemeVariants(MenuBarVariant.LUMO_ICON);
MenuItem item = menu.addItem(avatar);
SubMenu subMenu = item.getSubMenu();
subMenu.addItem(menuItem(FontAwesome.Solid.USER, "Profile", l -> Notification.show("Profile")));
if (isAdmin(user)) {
subMenu.addItem(menuItem(FontAwesome.Solid.COG, "Administration", l -> Notification.show("Administration")));
}
subMenu.addItem(menuItem(FontAwesome.Solid.QUESTION_CIRCLE, "Help", l -> Notification.show("Help")));
subMenu.addItem(menuItem(FontAwesome.Solid.SIGN_OUT, "Sign out", l -> authContext.logout()));
HorizontalLayout horizontalLayout = new HorizontalLayout();
horizontalLayout.setAlignItems(FlexComponent.Alignment.END);
horizontalLayout.add(logo, toggleTheme, menu);
addToNavbar(horizontalLayout);
}
private HorizontalLayout menuItem(FontAwesome.Solid icon, String title, ComponentEventListener<ClickEvent<HorizontalLayout>> listener) {
FontAwesome.Solid.Icon i = icon.create();
i.setSize(LumoProps.ICON_SIZE_S.var());
HorizontalLayout h = new HorizontalLayout();
h.setAlignItems(FlexComponent.Alignment.CENTER);
h.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
h.add(i);
h.add(title);
h.addClickListener(listener);
return h;
}
}
@@ -1,33 +0,0 @@
package de.grimsi.gameyfin.layouts;
import com.flowingcode.vaadin.addons.fontawesome.FontAwesome;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import de.grimsi.gameyfin.resources.PublicResources;
import de.grimsi.gameyfin.services.ThemeService;
import org.springframework.beans.factory.annotation.Autowired;
@CssImport("./styles/header.css")
public class SetupLayout extends AppLayout {
public SetupLayout(@Autowired ThemeService themeService) {
Image logo = new Image(PublicResources.GAMEYFIN_LOGO.path, "Gameyfin Logo");
logo.addClassName("header-logo");
Button toggleTheme = new Button(FontAwesome.Solid.CIRCLE_HALF_STROKE.create());
toggleTheme.addThemeVariants(ButtonVariant.LUMO_ICON);
toggleTheme.addClickListener(listener -> themeService.toggleTheme());
HorizontalLayout horizontalLayout = new HorizontalLayout();
horizontalLayout.setWidthFull();
horizontalLayout.setAlignSelf(FlexComponent.Alignment.END);
horizontalLayout.add(logo, toggleTheme);
addToNavbar(horizontalLayout);
}
}
@@ -1,11 +0,0 @@
package de.grimsi.gameyfin.resources;
public enum PublicResources {
GAMEYFIN_LOGO("public/images/Logo.svg");
public final String path;
PublicResources(String path) {
this.path = path;
}
}
@@ -1,11 +0,0 @@
package de.grimsi.gameyfin.services;
import com.vaadin.flow.component.notification.Notification;
import org.springframework.stereotype.Service;
@Service
public class ThemeService {
public void toggleTheme() {
Notification.show("Not implemented");
}
}
@@ -1,48 +0,0 @@
package de.grimsi.gameyfin.views;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.login.LoginForm;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.grimsi.gameyfin.resources.PublicResources;
import de.grimsi.gameyfin.setup.SetupService;
import org.springframework.beans.factory.annotation.Autowired;
@Route("login")
@PageTitle("Login")
@AnonymousAllowed
public class LoginView extends VerticalLayout implements BeforeEnterObserver {
private final LoginForm login = new LoginForm();
public LoginView(@Autowired SetupService setupService) {
if (!setupService.isSetupCompleted()) {
UI.getCurrent().navigate(SetupView.class);
UI.getCurrent().close();
}
Image logo = new Image(PublicResources.GAMEYFIN_LOGO.path, "Gameyfin");
logo.setHeight("100px");
login.setAction("login");
add(logo);
add(login);
}
@Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
if (beforeEnterEvent.getLocation()
.getQueryParameters()
.getParameters()
.containsKey("error")
) {
login.setError(true);
}
}
}
@@ -1,21 +0,0 @@
package de.grimsi.gameyfin.views;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.Pre;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import de.grimsi.gameyfin.layouts.MainLayout;
import jakarta.annotation.security.PermitAll;
@Route(value = "", layout = MainLayout.class)
@PageTitle("Gameyfin")
@PermitAll
public class MainView extends VerticalLayout {
public MainView() {
add(new H1("Gameyfin main page"));
add(new Pre("Work in progress"));
}
}
@@ -1,46 +0,0 @@
package de.grimsi.gameyfin.views;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.grimsi.gameyfin.layouts.SetupLayout;
import de.grimsi.gameyfin.setup.SetupService;
import org.springframework.beans.factory.annotation.Autowired;
@Route(value = "/setup", layout = SetupLayout.class)
@PageTitle("Setup")
@AnonymousAllowed
public class SetupView extends VerticalLayout {
public SetupView(@Autowired SetupService setupService) {
if (setupService.isSetupCompleted()) {
UI.getCurrent().navigate(LoginView.class);
UI.getCurrent().close();
}
setWidthFull();
setAlignItems(Alignment.CENTER);
add(new Text("Looks like it's your first time starting Gameyfin. Let's continue setting up your very own instance 🙂"));
TextField username = new TextField("Username");
username.focus();
PasswordField passwordField = new PasswordField("Password");
PasswordField passwordFieldRepeat = new PasswordField("Password (repeated)");
FormLayout form = new FormLayout();
form.add(new Text("Let's start with creating a super admin account. This account will have full permissions."));
form.add(username, passwordField, passwordFieldRepeat);
add(form);
}
}
@@ -1,7 +1,6 @@
package de.grimsi.gameyfin.config
import com.vaadin.flow.spring.security.VaadinWebSecurity
import de.grimsi.gameyfin.views.LoginView
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
@@ -15,7 +14,8 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher
@EnableWebSecurity
@Configuration
class SecurityConfiguration : VaadinWebSecurity() {
class SecurityConfig : VaadinWebSecurity() {
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
// Configure your static resources with public access before calling super.configure(HttpSecurity) as it adds final anyRequest matcher
@@ -23,18 +23,14 @@ class SecurityConfiguration : VaadinWebSecurity() {
auth.requestMatchers(AntPathRequestMatcher("/public/**")).permitAll()
}
// Configure your static resources with public access before calling
// super.configure(HttpSecurity) as it adds final anyRequest matcher
super.configure(http)
// This is important to register your login view to the navigation access control mechanism:
setLoginView(http, LoginView::class.java)
setLoginView(http, "/login")
}
@Throws(Exception::class)
public override fun configure(web: WebSecurity) {
super.configure(web)
web.ignoring().requestMatchers(AntPathRequestMatcher("/images/**"))
}
@Bean
@@ -26,17 +26,21 @@ class SetupDataLoader(
log.info { "We will now set up some data..." }
setupRoles()
//setupUser()
setupUsers()
log.info { "Setup completed..." }
}
fun setupUser() {
fun setupUsers() {
val superadmin = User("admin")
superadmin.password = "admin"
superadmin.roles = listOf(roleRepository.findByRolename(Roles.SUPERADMIN.roleName)!!)
userService.registerUser(superadmin)
val user = User("user")
user.password = "user"
user.roles = listOf(roleRepository.findByRolename(Roles.USER.roleName)!!)
userService.registerUser(user)
}
fun setupRoles() {
@@ -3,12 +3,14 @@ package de.grimsi.gameyfin.setup
import jakarta.servlet.*
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.core.annotation.Order
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import java.io.IOException
//@Order(1)
//@Component
@Order(1)
@Component
class SetupFilter(
private val setupService: SetupService
) : Filter {
@@ -0,0 +1,19 @@
package de.grimsi.gameyfin.users
import de.grimsi.gameyfin.users.dto.UserInfo
import dev.hilla.Endpoint
import jakarta.annotation.security.PermitAll
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.context.SecurityContextHolder
@Endpoint
class UserEndpoint {
@PermitAll
fun getUserInfo(): UserInfo {
val auth: Authentication = SecurityContextHolder.getContext().authentication
val authorities: List<String> = auth.authorities.map { g: GrantedAuthority -> g.authority }
return UserInfo(auth.name, authorities)
}
}
@@ -31,7 +31,7 @@ class UserService(
true,
true,
true,
getAuthorities(user.roles)
toAuthorities(user.roles)
)
}
@@ -40,7 +40,7 @@ class UserService(
return userRepository.save(user)
}
private fun getAuthorities(roles: Collection<Role>): List<GrantedAuthority> {
private fun toAuthorities(roles: Collection<Role>): List<GrantedAuthority> {
return roles.map { r -> SimpleGrantedAuthority(r.rolename) }
}
}
@@ -0,0 +1,6 @@
package de.grimsi.gameyfin.users.dto
data class UserInfo(
val name: String,
val authorities: List<String>
)
@@ -6,24 +6,25 @@ import jakarta.validation.constraints.NotNull
@Entity
@Table(name = "users")
class User(
@NotNull
@field:NotNull
var username: String,
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var id: Long? = null,
@NotNull
@field:NotNull
var password: String? = null,
@Nullable
@field:Nullable
var email: String? = null,
var enabled: Boolean = true,
@Embedded
@Nullable
@field:Nullable
var avatar: Avatar? = null,
@ManyToMany(fetch = FetchType.EAGER)

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+24 -10
View File
@@ -1,14 +1,28 @@
logging.level:
org.atmosphere: warn
server:
port: 8080
servlet:
session:
tracking-modes: cookie
spring:
# Workaround for https://github.dev/hilla/issues/842
devtools.restart.additional-exclude: dev/hilla/openapi.json
jpa:
defer-datasource-initialization: true
mustache:
check-template-location: false
sql.init.mode: always
application:
name: Gameyfin
vaadin:
launch-browser: true
# To improve the performance during development.
# For more information https://vaadin.com/docs/flow/spring/tutorial-spring-configuration.html#special-configuration-parameters
whitelisted-packages:
- com.vaadin
- org.vaadin
- dev.hilla
- com.flowingcode
frontend:
hotdeploy: false
spring:
jpa:
properties:
hibernate:
globally_quoted_identifiers: true
- dev.hilla
+9
View File
@@ -0,0 +1,9 @@
${AnsiBackground.DEFAULT}
${AnsiColor.BLUE} _____ ${AnsiColor.MAGENTA} ___ _
${AnsiColor.BLUE} / ___/ ___ _ __ _ ___ __ __${AnsiColor.MAGENTA} / _/ (_) ___
${AnsiColor.BLUE}/ (_ / / _ `/ / ' \/ -_) / // /${AnsiColor.MAGENTA} / _/ / / / _ \
${AnsiColor.BLUE}\___/ \_,_/ /_/_/_/\__/ \_, / ${AnsiColor.MAGENTA}/_/ /_/ /_//_/
${AnsiColor.BLUE} /___/
${AnsiColor.DEFAULT}
${spring.application.name} ${application.version}
@@ -1,13 +0,0 @@
package de.grimsi.gameyfin
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class GameyfinApplicationTests {
@Test
fun contextLoads() {
}
}