Reference Guide

SnapAdmin v0.2.1

1. Introduction

SnapAdmin is a tool to easily build customizable database management interfaces with CRUD operations (and more!) for Spring Boot+JPA apps.

It does so by scanning your JPA-annotated @Entity classes and building the required functionality at runtime. Since it won't generate actual code you won't have to change your existing code base, and this makes it easy to integrate. Moreover, every time you update your classes, all changes will be reflected automatically on the web UI.

On the other hand, this approach requires to interact correctly with all JPA annotations and adapt our behaviour accordingly. This is not an easy task given the large surface area of possible behaviours introduced with annotations and can sometimes introduce bugs. If you encounter problems, please report it as an issue on Github.

The rest of this guide outlines how to install, configure and customize SnapAdmin and, where applicable, it documents known interactions with JPA annotations. If, instead, you are looking for the Javadoc, you can find it here.

2. Getting started

Getting started with SnapAdmin requires including it as a dependency and minimal configuration.

2.1 Installation

Since SnapAdmin is distributed on Maven, the easiest way to start is to include it in your pom.xml:

<dependency>
	<groupId>tech.ailef</groupId>
	<artifactId>snap-admin</artifactId>
	<version>0.2.1</version>
</dependency>

TIP Go to the Maven repository to retrieve the exact snippet for the latest stable release.

Alternatively, if you want the latest unstable release, you can clone the main branch of the Github repo and execute mvn install -D skipTests in the project's directory. This will install the library in your local repository, which you can then include using the same snippet as above, but replacing the version number with the one contained in the pom.xml file of the code you pulled from Github.

2.2 Configuration

After including the dependency, a few configuration steps are required on your end in order to integrate it into your project.

Configure your application.properties file:

## SnapAdmin is not enabled by default
snapadmin.enabled=true

## The first-level part of the URL path: http://localhost:8080/${baseUrl}/
snapadmin.baseUrl=admin

## The package(s) that contain your @Entity classes
## accepts multiple comma separated values
snapadmin.modelsPackage=your.models.package,your.second.models.package

## At the moment, it's required to have open-in-view set to true.
# spring.jpa.open-in-view=true

## OPTIONAL PARAMETERS
## Whether to enable SnapAdmin
# snapadmin.enabled=true
#
## Set to true if you need to run the tests, as it will customize
## the database configuration for the internal DataSource
# snapadmin.testMode=false
#
## SQL console enable/disable (true by default)
# snapadmin.sqlConsoleEnabled=false

After this, you must tell Spring to import the SnapAdmin configuration. To do this, annotate your @SpringBootApplication class containing the main method with the following:

@ImportAutoConfiguration(SnapAdminAutoConfiguration.class)

This will autoconfigure the various SnapAdmin components when your application starts.

If everything is setup correctly, you will see SnapAdmin confirming it in the log messages that appear when you start your application. Keep in mind that if you specify the wrong models package, it will still work but provide you an empty interface. Visit http://localhost:8080/admin (replace the correct port and base path with your settings) to check if everything is working correctly.

2.2.1 Known issues

  • (SnapAdmin <= 0.1.8) Transactional: If you're using @Transactional methods, you need to annotate your transaction manager with the @Primary annotation, as you may otherwise get a NoUniqueBeanDefinitionException (read here for more info).

2.3 Supported features

2.3.1 Supported JPA annotations

  • Core: @Entity, @Table, @Column, @Lob, @Id, @GeneratedValue
  • Relationships: @OneToMany, @ManyToOne, @ManyToMany, @OneToOne
  • Validation: all JPA validation annotations (jakarta.validation.constraints.*)

The behaviours specified with these annotations should be applied automatically by SnapAdmin. Using non-supported annotations will not necessarily result in an error, as they are simply ignored. Depending on what the annotation actually does, this could be just fine or result in an error if it interferes with something that SnapAdmin relies on.

The following list documents the most significant interactions between JPA annotations and SnapAdmin.

Annotation name

Description

@Entity

Used to detect the candidate classes to scan.

@Column

Used to detect the column name and its nullability.

@GeneratedValue

When you have an @Id marked as a @GeneratedValue, you won't be asked to enter it when creating new items, as it will be automatically generated.

2.3.2 Supported field types

  • Double, Float, Integer, Short, Byte, Character, BigDecimal, BigInteger
  • Boolean
  • String, UUID
  • Date, LocalDate, LocalDateTime, OffsetDateTime, Instant
  • byte[]
  • Enum

For these field types, the interface will be already customized. For example, a file upload input is provided to fill a byte[] field or a date-picker for the various date types.

Unsupported field types are handled as gracefully as possible, meaning that when such a field is detected the application will still run. However, this field will never be displayed in the web interface. This means that it won't be possible to enter values for this field when editing or creating, leaving it with the default NULL value. If the field is not nullable, this implies you won't be able to create items.

If you're using a field type that you think should be supported, please open an issue on Github.

To check if your code contains unsupported fields:

  • In the SnapAdmin home page, a red icon is shown next to each table if problems have been detected; click this icon to get the detailed list of errors.
  • At startup, unsupported field types are printed in the logs (you should see them if you grep SnapAdmin).

3. Customization

There are two ways to customize the appearance and behaviour of SnapAdmin:

  1. Applying annotations on your @Entity classes, fields and methods
  2. Using the Settings panel through the web interface

Annotations are used primarily to customize behaviour and add custom logic to your classes. If, instead, you're looking to customize appearance of the web UI, it's most likley through the Settings panel.

3.1 Supported annotations

These annotations can be placed on classes, methods and fields to customize the behaviour of SnapAdmin.

3.1.1 @DisplayName

@DisplayName
public String getFullName() {
    return firstName + " " + lastName;
}	

When displaying a reference to an item, we show its primary key by default. If a class has a @DisplayName, this method will be used in addition to the primary key whenever possible, giving the user a more readable option.

3.1.2 @DisplayFormat

@DisplayFormat(format = "$%.2f")
private Double price;

Specify a format string to apply when displaying the field.

3.1.3 @ComputedColumn

Supported parameters
Name Type Required Description
name String false The name of this column in the web interface. The method's name is used if this value is not specified.
Code example
@ComputedColumn
public double numberOfOrders() {
    return orders.size();
}

This annotation can be used to add values computed at runtime that are shown like additional columns.

NOTE If your computed columns are computationally expensive (e.g because they use joins) they can affect the interface loading speed. In particular, the list view is the most affected, as these methods will get called for each item in the list.

3.1.4 @Filterable

Supported parameters
Name Required Type Description
type false Enum (DEFAULT, CATEGORICAL) If CATEGORICAL, this changes the filter in the UI to shows all the possible values directly instead of providing an autocomplete form.
Code example
@Filterable
private LocalDate createdAt;

@Filterable(type=FilterableType.CATEGORICAL)
@ManyToOne
private User user;

Place on one or more fields in a class to activate the faceted search feature. This will allow you to easily combine all these filters when operating on the table. Can only be placed on fields that correspond to physical columns on the table (e.g. no @ManyToMany/@OneToMany) and that are not binary (byte[]).

3.1.5 @DisplayImage

@DisplayImage
@Lob
private byte[] image;

This annotation can be placed on binary fields to declare they are storing an image and that we want it displayed when possible. The image will be shown as a small thumbnail.

3.1.6 @HiddenColumn

Code example
@HiddenColumn
private String cardNumber;


Marks a field as hidden. This column and its values will not be shown in the list and detail view for objects of this type. If the column is nullable, it will be hidden in the create and edit forms as well (and this will result in the column always being NULL when creating/editing objects). If, instead, it's not nullable column, it will be included in the create and edit forms as it would otherwise prevent the creation of items.

Please note that this is not meant as a security feature, but rather to hide uninformative columns that clutter the interface. In fact, since the create and edit form come pre-filled with all the information, these views will show the value of the hidden column (if it's not nullable).

3.1.7 @ReadOnly

Code example
@ReadOnly
private LocalDate createdAt;

Marks a field as read-only. The field can be filled at creation time, but it will be shown as disabled during edits, making it impossible to change its value after creation.

3.1.8 @DisableCreate, @DisableEdit, @DisableDelete

Code example
@Entity
@DisableCreate
public class Product { ... }

Disables the possibility of creating/editing/deleting items for the specific table.

3.1.9 @DisableExport

Code example
@Entity
@DisableExport
public class User { ... }

Disables the export functionality for this table.

3.1.10 @Disable

Code example
@Entity
@Disable
public class Payment { ... }

Disables SnapAdmin on this table, by ignoring it during the initialization phase.

3.2 The Settings panel

As mentioned earlier, the Settings panel primarily provides options to customize the branding/appearance of the web interface. These settings are persistent across restarts and are stored in an embedded H2 database (file named snapadmin_internal), along with other data required by SnapAdmin.

4. Security

SnapAdmin does not implement authentication and/or authorization mechanisms. However, you can use a standard Spring Security configuration in order to limit access to the web UI or specific parts of it.

All SnapAdmin routes start with the value of snapadmin.baseUrl property, and all write operations (edit, create, delete) are implemented as POST calls. The following code provides an example security configuration thatyou can further customize:

@Autowired
private SnapAdminProperties properties;

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    String baseUrl = properties.getBaseUrl();
	
    return http.authorizeHttpRequests(auth -> {
        /* POST methods (create, edit and delete) require ADMIN role
    	 * Note that with this configuration users will still be able to access the edit/create page
    	 * but they will get a Forbidden error after submitting the form if they are not authorized.
    	 * You can also stop the serving of these pages altogether by customizing the route matchers
    	 */
        auth.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/" + baseUrl + "/**"))
        	.hasAuthority("ADMIN")
            // Read-only SnapAdmin routes require authentication (any role)
            .requestMatchers(AntPathRequestMatcher.antMatcher("/" + baseUrl + "/**"))
            .authenticated()
            // The other routes are not protected (adapt to your needs)
            .requestMatchers(AntPathRequestMatcher.antMatcher("/**")).permitAll();
    })
    .formLogin(l -> l.loginPage("/login").permitAll())
    /* This custom exception handling code is only needed if you want to have
     * nicer Forbidden error pages, for cases when a user tries to perform an
     * action they don't have the correct privileges for (e.g., in the previous
     * configuration a user without ADMIN role trying to edit/create items).
     * The exception handling is delegated to the default handler if the
     * error didn't occur on a SnapAdmin route. You can further customize this
     * according to your needs or just not use it. In this last scenario, your 
     * default access denied handler will be used even for errors occurring inside
     * SnapAdmin.
     */
    .exceptionHandling(e -> e.accessDeniedHandler((req, res, ex) -> {
    	AccessDeniedHandlerImpl defaultHandler = new AccessDeniedHandlerImpl();
    	if (req.getServletPath().toString().startsWith("/" + baseUrl + "/")) {
    		res.sendRedirect("/" + baseUrl + "/forbidden");
    	} else {
    		defaultHandler.handle(req, res, ex);
    	}
    }))
    .build();
}

Authorization

If your authentication related classes (e.g. User and Role) are managed by SnapAdmin it might be possible to edit them, depending on your security configuration. This means that it might be possible for a USER to edit their own roles and escalate their privileges to ADMIN. To setup things correctly so that this can't happen you have a few options:

Exclude auth-related classes from SnapAdmin

If you don't want these classes to be accessible at all from the UI, the best option is to just place them in a package that's not managed by SnapAdmin. Alternatively, if changing the package structure is not an option, you can apply them the @Disable annotation to obtain an equivalent result.

Adapt SecurityConfiguration

If you want these classes to be accessible from SnapAdmin securely, you need to change your security configuration so that write operations on these tables can only be performed by users with the right privileges. For example, you can add something like this to your security configuration:

.requestMatchers(
    AntPathRequestMatcher.antMatcher("/" + baseUrl + "/model/tech.ailef.snapadmin.auth.models.User")
).hasAuthority("ADMIN")
.requestMatchers(
    AntPathRequestMatcher.antMatcher("/" + baseUrl + "/model/tech.ailef.snapadmin.auth.models.Role")
).hasAuthority("ADMIN")

With this configuration, only ADMIN users are allowed to access the SnapAdmin routes for the protected tables. As always, you can customize this further to your needs as long as you follow these guidelines. You can look at the SnapAdmin auth test project to see a full implementation and use it as a blueprint for your own projects.

5. Troubleshooting

When setting up SnapAdmin for the first time, problems most commonly occur at startup, causing the application to stop. If this is the case, first check that you have correctly configured your application.properties file. If everything is correct here, the problem might be related to one or more of your @Entity classes (or the rest of your code in general) which might be using some unsupported feature and/or annotation.

You can enable DEBUG-level logs (e.g. with logging.level.root=DEBUG) to pinpoint the cause error. Looking at those in combination with the stack trace should provide enough information to understand what is going wrong. Keep in mind that if the application doesn't start at all, it's probably a bug: you can open an issue on Github, providing the stacktrace, the debug logs and all other relevant information.