diff --git a/README.md b/README.md index 87defbe..bc74ea6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ # Spring Boot Database Admin Panel -An add-on for Spring Boot apps that automatically generates a database admin panel based on your `@Entity` annotated classes. -The panel offers basic CRUD and search functionalities to manage the database. +Generate a powerful CRUD management dashboard for your Spring Boot application in a few minutes. + +Spring Boot Database Admin scans your `@Entity` classes to generate a simple but powerful database management interface. [![Example page listing products](https://i.imgur.com/Nz19f8e.png)](https://i.imgur.com/Nz19f8e.png) +Features: + * List objects with pagination and sorting + * Show detailed object page which also includes `@OneToMany`, `@ManyToMany`, etc... fields + * Create/Edit objects + * Search + * Customization + The code is in a very early version and I'm trying to collect as much feedback as possible in order to fix the most common issues that will inevitably arise. If you are so kind to try the project and you find something broken, please report it as an issue and I will try to take a look at it. @@ -17,44 +25,40 @@ broken, please report it as an issue and I will try to take a look at it. tech.ailef spring-boot-db-admin - 0.0.1-SNAPSHOT + 0.0.4 ``` -2. A few configuration steps are required on your code in order to integrate the library in your project. If you don't want +2. A few simple configuration steps are required on your end in order to integrate the library into your project. If you don't want to test on your own code, you can clone the [test project](https://github.com/aileftech/spring-boot-database-admin-test) which provides a sample database and already configured code. -If you wish to integrate it into your project instead, the first step is create creating a configuration class: +If you wish to integrate it into your project instead, the first step is adding these to your `application.properties` file: ``` -@DbAdminConfiguration -@Configuration -public class TestConfiguration implements DbAdminAppConfiguration { +# Optional, default true +dbadmin.enabled=true - @Override - public String getModelsPackage() { - return "your.models.package"; // The package where your @Entity classes are located - } -} +# The first-level part of the URL path: http://localhost:8080/${baseUrl}/ +dbadmin.baseUrl=admin + +# The package that contains your @Entity classes +dbadmin.modelsPackage=tech.ailef.dbadmin.test.models ``` The last step is to annotate your `@SpringBootApplication` class containing the `main` method with the following: ``` -@ComponentScan(basePackages = {"your.project.root.package", "tech.ailef.dbadmin"}) -@EnableJpaRepositories(basePackages = {"your.project.root.package", "tech.ailef.dbadmin"}) -@EntityScan(basePackages = {"your.project.root.package", "tech.ailef.dbadmin"}) +@ImportAutoConfiguration(DbAdminAutoConfiguration.class) ``` -This tells Spring to scan the `tech.ailef.dbadmin` packages and look for components there as well. Remember to also include -your original root package as shown, or Spring will not scan it otherwise. +This will autoconfigure the various DbAdmin components when your application starts. -3. At this point, when you run your application, you should be able to visit `http://localhost:$PORT/dbadmin` and access the web interface. +3. At this point, when you run your application, you should be able to visit `http://localhost:$PORT/${baseUrl}` and access the web interface. ## Documentation -Once you are correctly running Spring Boot Database Admin you will see the web interface at `http://localhost:$PORT/dbadmin`. Most of the features are already available with the basic configuration. However, some customization to the interface might be applied by using appropriate annotations on your classes fields or methods. +Once you are correctly running Spring Boot Database Admin, you will be able to access the web interface. Most of the features are already available with the basic configuration. However, some customization to the interface might be applied by using appropriate annotations on your classes fields or methods. The following annotations are supported. ### @DisplayName diff --git a/pom.xml b/pom.xml index c1af3e5..c210c58 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ tech.ailef spring-boot-db-admin - 0.0.3 + 0.0.4 spring-boot-db-admin Srping Boot DB Admin Dashboard diff --git a/src/main/java/tech/ailef/dbadmin/ApplicationContextUtils.java b/src/main/java/tech/ailef/dbadmin/ApplicationContextUtils.java deleted file mode 100644 index 0529431..0000000 --- a/src/main/java/tech/ailef/dbadmin/ApplicationContextUtils.java +++ /dev/null @@ -1,23 +0,0 @@ -package tech.ailef.dbadmin; - -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.stereotype.Component; - -/** - * Utility class the get the ApplicationContext - */ -@Component -public class ApplicationContextUtils implements ApplicationContextAware { - - private static ApplicationContext ctx; - - @Override - public void setApplicationContext(ApplicationContext appContext) { - ctx = appContext; - } - - public static ApplicationContext getApplicationContext() { - return ctx; - } -} \ No newline at end of file diff --git a/src/main/java/tech/ailef/dbadmin/DbAdmin.java b/src/main/java/tech/ailef/dbadmin/DbAdmin.java index c96fff3..35dc295 100644 --- a/src/main/java/tech/ailef/dbadmin/DbAdmin.java +++ b/src/main/java/tech/ailef/dbadmin/DbAdmin.java @@ -6,11 +6,13 @@ import java.lang.reflect.ParameterizedType; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Set; +import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.stereotype.Component; @@ -25,8 +27,6 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; import jakarta.persistence.PersistenceContext; -import tech.ailef.dbadmin.annotations.DbAdminAppConfiguration; -import tech.ailef.dbadmin.annotations.DbAdminConfiguration; import tech.ailef.dbadmin.annotations.DisplayFormat; import tech.ailef.dbadmin.dbmapping.AdvancedJpaRepository; import tech.ailef.dbadmin.dbmapping.DbField; @@ -46,6 +46,8 @@ import tech.ailef.dbadmin.misc.Utils; */ @Component public class DbAdmin { + private static final Logger logger = Logger.getLogger(DbAdmin.class.getName()); + @PersistenceContext private EntityManager entityManager; @@ -53,17 +55,8 @@ public class DbAdmin { private String modelsPackage; - public DbAdmin(@Autowired EntityManager entityManager) { - Map beansWithAnnotation = - ApplicationContextUtils.getApplicationContext().getBeansWithAnnotation(DbAdminConfiguration.class); - - if (beansWithAnnotation.size() != 1) { - throw new DbAdminException("Found " + beansWithAnnotation.size() + " beans with annotation @DbAdminConfiguration, but must be unique"); - } - - DbAdminAppConfiguration applicationClass = (DbAdminAppConfiguration) beansWithAnnotation.values().iterator().next(); - - this.modelsPackage = applicationClass.getModelsPackage(); + public DbAdmin(@Autowired EntityManager entityManager, @Autowired DbAdminProperties properties) { + this.modelsPackage = properties.getModelsPackage(); this.entityManager = entityManager; ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); @@ -73,6 +66,9 @@ public class DbAdmin { for (BeanDefinition bd : beanDefs) { schemas.add(processBeanDefinition(bd)); } + + logger.info("Spring Boot Database Admin initialized. Loaded " + schemas.size() + " table definitions"); + logger.info("Spring Boot Database Admin web interface at: http://YOUR_HOST:YOUR_PORT/" + properties.getBaseUrl()); } /** diff --git a/src/main/java/tech/ailef/dbadmin/DbAdminAutoConfiguration.java b/src/main/java/tech/ailef/dbadmin/DbAdminAutoConfiguration.java new file mode 100644 index 0000000..2b54652 --- /dev/null +++ b/src/main/java/tech/ailef/dbadmin/DbAdminAutoConfiguration.java @@ -0,0 +1,14 @@ +package tech.ailef.dbadmin; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; + +@ConditionalOnProperty(name = "dbadmin.enabled", matchIfMissing = true) +@ComponentScan +@EnableConfigurationProperties(DbAdminProperties.class) +@AutoConfiguration +public class DbAdminAutoConfiguration { + +} \ No newline at end of file diff --git a/src/main/java/tech/ailef/dbadmin/DbAdminProperties.java b/src/main/java/tech/ailef/dbadmin/DbAdminProperties.java new file mode 100644 index 0000000..5d1bf88 --- /dev/null +++ b/src/main/java/tech/ailef/dbadmin/DbAdminProperties.java @@ -0,0 +1,51 @@ +package tech.ailef.dbadmin; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * The 'dbadmin.*' properties that can be set in the properties file + * to configure the behaviour of Spring Boot Admin Panel. + */ +@ConfigurationProperties("dbadmin") +public class DbAdminProperties { + /** + * Whether Spring Boot Database Admin is enabled. + */ + public boolean enabled = true; + + /** + * The prefix that is prepended to all routes registered by Spring Boot Database Admin. + */ + private String baseUrl; + + /** + * The path of the package that contains your JPA `@Entity` classes to be scanned. + */ + private String modelsPackage; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public String getModelsPackage() { + return modelsPackage; + } + + public void setModelsPackage(String modelsPackage) { + this.modelsPackage = modelsPackage; + } + + +} diff --git a/src/main/java/tech/ailef/dbadmin/annotations/DbAdminAppConfiguration.java b/src/main/java/tech/ailef/dbadmin/annotations/DbAdminAppConfiguration.java deleted file mode 100644 index ee53fa7..0000000 --- a/src/main/java/tech/ailef/dbadmin/annotations/DbAdminAppConfiguration.java +++ /dev/null @@ -1,10 +0,0 @@ -package tech.ailef.dbadmin.annotations; - -/** - * An interface that includes all the configuration methods that - * the user has to implement in order to integrate DbAdmin. - * - */ -public interface DbAdminAppConfiguration { - public String getModelsPackage(); -} diff --git a/src/main/java/tech/ailef/dbadmin/annotations/DbAdminConfiguration.java b/src/main/java/tech/ailef/dbadmin/annotations/DbAdminConfiguration.java deleted file mode 100644 index 3050ffe..0000000 --- a/src/main/java/tech/ailef/dbadmin/annotations/DbAdminConfiguration.java +++ /dev/null @@ -1,15 +0,0 @@ -package tech.ailef.dbadmin.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks the class that holds the DbAdmin configuration. - * - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface DbAdminConfiguration { -} \ No newline at end of file diff --git a/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java b/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java index f42e14b..0f1bd7b 100644 --- a/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java +++ b/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java @@ -28,6 +28,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import tech.ailef.dbadmin.DbAdmin; +import tech.ailef.dbadmin.DbAdminProperties; import tech.ailef.dbadmin.dbmapping.DbAdminRepository; import tech.ailef.dbadmin.dbmapping.DbObject; import tech.ailef.dbadmin.dbmapping.DbObjectSchema; @@ -41,8 +42,11 @@ import tech.ailef.dbadmin.misc.Utils; * The main DbAdmin controller that register most of the routes of the web interface. */ @Controller -@RequestMapping("/dbadmin") +@RequestMapping(value= {"/${dbadmin.baseUrl}", "/${dbadmin.baseUrl}/"}) public class DefaultDbAdminController { + @Autowired + private DbAdminProperties properties; + @Autowired private DbAdminRepository repository; @@ -163,7 +167,7 @@ public class DefaultDbAdminController { return "model/list"; } catch (InvalidPageException e) { - return "redirect:/dbadmin/model/" + className; + return "redirect:/" + properties.getBaseUrl() + "/model/" + className; } } @@ -260,7 +264,7 @@ public class DefaultDbAdminController { attr.addFlashAttribute("error", e.getMessage()); } - return "redirect:/dbadmin/model/" + className; + return "redirect:/" + properties.getBaseUrl() + "/model/" + className; } @PostMapping(value="/model/{className}/delete") @@ -287,7 +291,7 @@ public class DefaultDbAdminController { if (countDeleted > 0) attr.addFlashAttribute("message", "Deleted " + countDeleted + " of " + ids.length + " items"); - return "redirect:/dbadmin/model/" + className; + return "redirect:/" + properties.getBaseUrl() + "/model/" + className; } @PostMapping(value="/model/{className}/create") @@ -397,11 +401,11 @@ public class DefaultDbAdminController { if (attr.getFlashAttributes().containsKey("error")) { if (create) - return "redirect:/dbadmin/model/" + schema.getClassName() + "/create"; + return "redirect:/" + properties.getBaseUrl() + "/model/" + schema.getClassName() + "/create"; else - return "redirect:/dbadmin/model/" + schema.getClassName() + "/edit/" + pkValue; + return "redirect:/" + properties.getBaseUrl() + "/model/" + schema.getClassName() + "/edit/" + pkValue; } else { - return "redirect:/dbadmin/model/" + schema.getClassName() + "/show/" + pkValue; + return "redirect:/" + properties.getBaseUrl() + "/model/" + schema.getClassName() + "/show/" + pkValue; } } diff --git a/src/main/java/tech/ailef/dbadmin/controller/DownloadController.java b/src/main/java/tech/ailef/dbadmin/controller/DownloadController.java index 7542cc0..aef8b27 100644 --- a/src/main/java/tech/ailef/dbadmin/controller/DownloadController.java +++ b/src/main/java/tech/ailef/dbadmin/controller/DownloadController.java @@ -28,7 +28,7 @@ import tech.ailef.dbadmin.exceptions.DbAdminException; * Controller to serve file or images (`@DisplayImage`) */ @Controller -@RequestMapping("/dbadmin/download") +@RequestMapping(value = {"/${dbadmin.baseUrl}/download", "/${dbadmin.baseUrl}/download/"}) public class DownloadController { @Autowired private DbAdminRepository repository; diff --git a/src/main/java/tech/ailef/dbadmin/controller/GlobalController.java b/src/main/java/tech/ailef/dbadmin/controller/GlobalController.java index c900079..d90239a 100644 --- a/src/main/java/tech/ailef/dbadmin/controller/GlobalController.java +++ b/src/main/java/tech/ailef/dbadmin/controller/GlobalController.java @@ -2,10 +2,12 @@ package tech.ailef.dbadmin.controller; import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ModelAttribute; import jakarta.servlet.http.HttpServletRequest; +import tech.ailef.dbadmin.DbAdminProperties; /** * This class registers some ModelAttribute objects that are @@ -14,6 +16,9 @@ import jakarta.servlet.http.HttpServletRequest; @ControllerAdvice public class GlobalController { + @Autowired + private DbAdminProperties props; + /** * A multi valued map containing the query parameters. It is used primarily * in building complex URL when performing faceted search with multiple filters. @@ -24,4 +29,14 @@ public class GlobalController { public Map getQueryParams(HttpServletRequest request) { return request.getParameterMap(); } + + /** + * The baseUrl as specified in the properties file by the user + * @param request + * @return + */ + @ModelAttribute("baseUrl") + public String getBaseUrl(HttpServletRequest request) { + return props.getBaseUrl(); + } } \ No newline at end of file diff --git a/src/main/java/tech/ailef/dbadmin/controller/rest/AutocompleteController.java b/src/main/java/tech/ailef/dbadmin/controller/rest/AutocompleteController.java index b39d19b..a1e2f6f 100644 --- a/src/main/java/tech/ailef/dbadmin/controller/rest/AutocompleteController.java +++ b/src/main/java/tech/ailef/dbadmin/controller/rest/AutocompleteController.java @@ -20,7 +20,7 @@ import tech.ailef.dbadmin.dto.AutocompleteSearchResult; * API controller for autocomplete results */ @RestController -@RequestMapping("/dbadmin/api/autocomplete") +@RequestMapping(value= {"/${dbadmin.baseUrl}/api/autocomplete", "/${dbadmin.baseUrl}/api/autocomplete/"}) public class AutocompleteController { @Autowired private DbAdmin dbAdmin; diff --git a/src/main/java/tech/ailef/dbadmin/controller/rest/DefaultDbAdminRestController.java b/src/main/java/tech/ailef/dbadmin/controller/rest/DefaultDbAdminRestController.java index 308eae5..a14bccb 100644 --- a/src/main/java/tech/ailef/dbadmin/controller/rest/DefaultDbAdminRestController.java +++ b/src/main/java/tech/ailef/dbadmin/controller/rest/DefaultDbAdminRestController.java @@ -19,7 +19,7 @@ import tech.ailef.dbadmin.dto.PaginatedResult; import tech.ailef.dbadmin.exceptions.DbAdminException; @RestController -@RequestMapping("/dbadmin/api") +@RequestMapping(value = {"/${dbadmin.baseUrl}/api", "/${dbadmin.baseUrl}/api/"}) public class DefaultDbAdminRestController { @Autowired public DbAdmin dbAdmin; diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java b/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java index a227ef2..d94bc46 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java @@ -36,7 +36,6 @@ public class DbAdminRepository { public DbAdminRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; - } /** @@ -119,7 +118,7 @@ public class DbAdminRepository { return new PaginatedResult( - new PaginationInfo(page, maxPage, pageSize, maxElement, null, sortKey, sortOrder, new HashSet<>()), + new PaginationInfo(page, maxPage, pageSize, maxElement, null, new HashSet<>()), results ); } @@ -229,7 +228,7 @@ public class DbAdminRepository { } return new PaginatedResult( - new PaginationInfo(page, maxPage, pageSize, maxElement, query, sortKey, sortOrder, queryFilters), + new PaginationInfo(page, maxPage, pageSize, maxElement, query, queryFilters), jpaRepository.search(query, page, pageSize, sortKey, sortOrder, queryFilters).stream() .map(o -> new DbObject(o, schema)) .toList() diff --git a/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java b/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java index dd69835..11cc31a 100644 --- a/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java +++ b/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java @@ -41,20 +41,13 @@ public class PaginationInfo { private String query; - private String sortKey; - - private String sortOrder; - - public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement, String query, - String sortKey, String sortOrder, Set queryFilters) { + public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement, String query, Set queryFilters) { this.currentPage = currentPage; this.maxPage = maxPage; this.pageSize = pageSize; this.query = query; this.maxElement = maxElement; this.queryFilters = queryFilters; - this.sortKey = sortKey; - this.sortOrder = sortOrder; } public int getCurrentPage() { @@ -122,7 +115,14 @@ public class PaginationInfo { public List getAfterPages() { return IntStream.range(currentPage + 1, Math.min(currentPage + PAGE_RANGE, maxPage + 1)).boxed().collect(Collectors.toList()); } - +// +// public String getSortKey() { +// return sortKey; +// } +// +// public String getSortOrder() { +// return sortOrder; +// } public boolean isLastPage() { return currentPage == maxPage; diff --git a/src/main/resources/templates/fragments/data_row.html b/src/main/resources/templates/fragments/data_row.html index b9b22cb..a62978d 100644 --- a/src/main/resources/templates/fragments/data_row.html +++ b/src/main/resources/templates/fragments/data_row.html @@ -8,10 +8,10 @@ th:value="${row.getPrimaryKeyValue()}" form="multi-delete-form"> - +
+ th:action="|/${baseUrl}/model/${schema.getJavaClass().getName()}/delete/${row.getPrimaryKeyValue()}|">
@@ -34,7 +34,7 @@ - +

- + @@ -63,11 +63,11 @@

+ th:src="|/${baseUrl}/download/${schema.getClassName()}/${field.getJavaName()}/${object.getPrimaryKeyValue()}/image|">
+ th:href="|/${baseUrl}/download/${schema.getClassName()}/${field.getJavaName()}/${object.getPrimaryKeyValue()}|"> Download ([[ ${object.get(field).getValue().length} ]] bytes) diff --git a/src/main/resources/templates/fragments/resources.html b/src/main/resources/templates/fragments/resources.html index fbfca31..c51cbcb 100644 --- a/src/main/resources/templates/fragments/resources.html +++ b/src/main/resources/templates/fragments/resources.html @@ -38,7 +38,7 @@