From 774c862ab38e702985a8204b1bf5026495aea45c Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 20 Sep 2023 12:06:35 +0200 Subject: [PATCH 1/4] 0.0.2 --- pom.xml | 2 +- src/main/java/tech/ailef/dbadmin/DbAdmin.java | 1 - .../ailef/dbadmin/annotations/Filterable.java | 11 ++ .../controller/DefaultDbAdminController.java | 63 ++++++- .../dbadmin/controller/GlobalController.java | 27 +++ .../dbmapping/AdvancedJpaRepository.java | 162 +++++++++++++++--- .../dbadmin/dbmapping/DbAdminRepository.java | 47 +++-- .../ailef/dbadmin/dbmapping/DbFieldType.java | 73 ++++++++ .../ailef/dbadmin/dbmapping/DbFieldValue.java | 9 +- .../ailef/dbadmin/dbmapping/DbObject.java | 7 +- .../dbadmin/dbmapping/DbObjectSchema.java | 97 +---------- .../ailef/dbadmin/dto/CompareOperator.java | 52 ++++++ .../ailef/dbadmin/dto/ListModelRequest.java | 37 ++++ .../ailef/dbadmin/dto/PaginationInfo.java | 51 +++++- .../tech/ailef/dbadmin/dto/QueryFilter.java | 57 ++++++ .../java/tech/ailef/dbadmin/misc/Utils.java | 72 ++++++++ src/main/resources/application.properties | 8 +- src/main/resources/static/css/dbadmin.css | 19 ++ src/main/resources/static/js/filters.js | 34 ++++ .../templates/fragments/data_row.html | 17 +- .../resources/templates/fragments/forms.html | 68 ++++++++ .../templates/fragments/resources.html | 37 +++- .../templates/fragments/table_selectable.html | 13 +- .../resources/templates/model/create.html | 4 +- src/main/resources/templates/model/list.html | 82 +++++++-- 25 files changed, 858 insertions(+), 192 deletions(-) create mode 100644 src/main/java/tech/ailef/dbadmin/annotations/Filterable.java create mode 100644 src/main/java/tech/ailef/dbadmin/controller/GlobalController.java create mode 100644 src/main/java/tech/ailef/dbadmin/dto/CompareOperator.java create mode 100644 src/main/java/tech/ailef/dbadmin/dto/ListModelRequest.java create mode 100644 src/main/java/tech/ailef/dbadmin/dto/QueryFilter.java create mode 100644 src/main/resources/static/js/filters.js diff --git a/pom.xml b/pom.xml index 3198ff9..61171a0 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ tech.ailef spring-boot-db-admin - 0.0.1-SNAPSHOT + 0.0.2 spring-boot-db-admin Srping Boot DB Admin Dashboard diff --git a/src/main/java/tech/ailef/dbadmin/DbAdmin.java b/src/main/java/tech/ailef/dbadmin/DbAdmin.java index f92e22d..c0c3427 100644 --- a/src/main/java/tech/ailef/dbadmin/DbAdmin.java +++ b/src/main/java/tech/ailef/dbadmin/DbAdmin.java @@ -95,7 +95,6 @@ public class DbAdmin { field.setSchema(schema); schema.addField(field); - System.out.println(field); } return schema; diff --git a/src/main/java/tech/ailef/dbadmin/annotations/Filterable.java b/src/main/java/tech/ailef/dbadmin/annotations/Filterable.java new file mode 100644 index 0000000..5b72c56 --- /dev/null +++ b/src/main/java/tech/ailef/dbadmin/annotations/Filterable.java @@ -0,0 +1,11 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Filterable { +} \ 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 99942d1..f002e35 100644 --- a/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java +++ b/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; @@ -12,6 +13,7 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -22,12 +24,17 @@ import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; 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.dbmapping.DbAdminRepository; import tech.ailef.dbadmin.dbmapping.DbObject; import tech.ailef.dbadmin.dbmapping.DbObjectSchema; +import tech.ailef.dbadmin.dto.CompareOperator; import tech.ailef.dbadmin.dto.PaginatedResult; +import tech.ailef.dbadmin.dto.QueryFilter; import tech.ailef.dbadmin.exceptions.InvalidPageException; +import tech.ailef.dbadmin.misc.Utils; @Controller @RequestMapping("/dbadmin") @@ -45,10 +52,15 @@ import tech.ailef.dbadmin.exceptions.InvalidPageException; * - Pagination in one to many results? * - BLOB upload (WIP: check edit not working) * - AI console (PRO) + * - Action logs + * - Boolean icons + * - @Filterable + * - Boolean in create/edit is checkbox * - SQL console (PRO) * - JPA Validation (PRO) * - Logging - * - ERROR 500: http://localhost:8080/dbadmin/model/tech.ailef.dbadmin.test.models.Order?query=2021 + * - TODO FIX: list model page crash + * EDIT error on table product * - Logs in web ui * - Tests: AutocompleteController, REST API, create/edit */ @@ -86,16 +98,55 @@ public class DefaultDbAdminController { public String list(Model model, @PathVariable String className, @RequestParam(required=false) Integer page, @RequestParam(required=false) String query, @RequestParam(required=false) Integer pageSize, @RequestParam(required=false) String sortKey, - @RequestParam(required=false) String sortOrder) { + @RequestParam(required=false) String sortOrder, @RequestParam MultiValueMap otherParams, + HttpServletRequest request, + HttpServletResponse response) { + if (page == null) page = 1; if (pageSize == null) pageSize = 50; + Set queryFilters = Utils.computeFilters(otherParams); + if (otherParams.containsKey("remove_field")) { + List fields = otherParams.get("remove_field"); + + for (int i = 0; i < fields.size(); i++) { + QueryFilter toRemove = + new QueryFilter( + fields.get(i), + CompareOperator.valueOf(otherParams.get("remove_op").get(i).toUpperCase()), + otherParams.get("remove_value").get(i) + ); + queryFilters.removeIf(f -> f.equals(toRemove)); + } + + MultiValueMap parameterMap = Utils.computeParams(queryFilters); + + MultiValueMap filteredParams = new LinkedMultiValueMap<>(); + request.getParameterMap().entrySet().stream() + .filter(e -> !e.getKey().startsWith("remove_") && !e.getKey().startsWith("filter_")) + .forEach(e -> { + filteredParams.putIfAbsent(e.getKey(), new ArrayList<>()); + for (String v : e.getValue()) { + if (filteredParams.get(e.getKey()).isEmpty()) { + filteredParams.get(e.getKey()).add(v); + } else { + filteredParams.get(e.getKey()).set(0, v); + } + } + }); + + filteredParams.putAll(parameterMap); + String queryString = Utils.getQueryString(filteredParams); + String redirectUrl = request.getServletPath() + queryString; + return "redirect:" + redirectUrl.trim(); + } + DbObjectSchema schema = dbAdmin.findSchemaByClassName(className); try { PaginatedResult result = null; - if (query != null) { - result = repository.search(schema, query, page, pageSize, sortKey, sortOrder); + if (query != null || !otherParams.isEmpty()) { + result = repository.search(schema, query, page, pageSize, sortKey, sortOrder, queryFilters); } else { result = repository.findAll(schema, page, pageSize, sortKey, sortOrder); } @@ -107,6 +158,7 @@ public class DefaultDbAdminController { model.addAttribute("sortKey", sortKey); model.addAttribute("query", query); model.addAttribute("sortOrder", sortOrder); + model.addAttribute("activeFilters", queryFilters); return "model/list"; } catch (InvalidPageException e) { @@ -333,9 +385,12 @@ public class DefaultDbAdminController { } } + @GetMapping("/settings") public String settings(Model model) { model.addAttribute("activePage", "settings"); return "settings"; } + + } diff --git a/src/main/java/tech/ailef/dbadmin/controller/GlobalController.java b/src/main/java/tech/ailef/dbadmin/controller/GlobalController.java new file mode 100644 index 0000000..c900079 --- /dev/null +++ b/src/main/java/tech/ailef/dbadmin/controller/GlobalController.java @@ -0,0 +1,27 @@ +package tech.ailef.dbadmin.controller; + +import java.util.Map; + +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * This class registers some ModelAttribute objects that are + * used in all templates. + */ +@ControllerAdvice +public class GlobalController { + + /** + * A multi valued map containing the query parameters. It is used primarily + * in building complex URL when performing faceted search with multiple filters. + * @param request the incoming request + * @return multi valued map of request parameters + */ + @ModelAttribute("queryParams") + public Map getQueryParams(HttpServletRequest request) { + return request.getParameterMap(); + } +} \ No newline at end of file diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java b/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java index 6eb67c8..e5db4ba 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java @@ -1,17 +1,29 @@ package tech.ailef.dbadmin.dbmapping; +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import org.springframework.data.jpa.repository.support.SimpleJpaRepository; +import org.springframework.web.multipart.MultipartFile; import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import tech.ailef.dbadmin.dto.CompareOperator; +import tech.ailef.dbadmin.dto.QueryFilter; +import tech.ailef.dbadmin.exceptions.DbAdminException; @SuppressWarnings("rawtypes") public class AdvancedJpaRepository extends SimpleJpaRepository { @@ -27,51 +39,151 @@ public class AdvancedJpaRepository extends SimpleJpaRepository { this.schema = schema; } - public long count(String q) { + @SuppressWarnings("unchecked") + public long count(String q, Set queryFilters) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Long.class); Root root = query.from(schema.getJavaClass()); - - List stringFields = - schema.getSortedFields().stream().filter(f -> f.getType() == DbFieldType.STRING) - .collect(Collectors.toList()); - - System.out.println("STRING F = " + stringFields); - List predicates = new ArrayList<>(); - for (DbField f : stringFields) { - Path path = root.get(f.getJavaName()); - predicates.add(cb.like(cb.lower(cb.toString(path)), "%" + q.toLowerCase() + "%")); - } + List finalPredicates = buildPredicates(q, queryFilters, cb, root); + query.select(cb.count(root.get(schema.getPrimaryKey().getName()))) - .where(cb.or(predicates.toArray(new Predicate[predicates.size()]))); + .where( + cb.and( + finalPredicates.toArray(new Predicate[finalPredicates.size()]) + ) + ); Object o = entityManager.createQuery(query).getSingleResult(); return (Long)o; } @SuppressWarnings("unchecked") - public List search(String q, int page, int pageSize, String sortKey, String sortOrder) { - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + public List search(String q, int page, int pageSize, String sortKey, String sortOrder, Set filters) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(schema.getJavaClass()); Root root = query.from(schema.getJavaClass()); - List stringFields = - schema.getSortedFields().stream().filter(f -> f.getType() == DbFieldType.STRING) - .collect(Collectors.toList()); + List finalPredicates = buildPredicates(q, filters, cb, root); - List predicates = new ArrayList<>(); - for (DbField f : stringFields) { - Path path = root.get(f.getJavaName()); - predicates.add(cb.like(cb.lower(cb.toString(path)), "%" + q.toLowerCase() + "%")); - } - query.select(root) - .where(cb.or(predicates.toArray(new Predicate[predicates.size()]))); + .where( + cb.and( + finalPredicates.toArray(new Predicate[finalPredicates.size()]) // query search on String fields + ) + + ); if (sortKey != null) query.orderBy(sortOrder.equals("DESC") ? cb.desc(root.get(sortKey)) : cb.asc(root.get(sortKey))); return entityManager.createQuery(query).setMaxResults(pageSize) .setFirstResult((page - 1) * pageSize).getResultList(); } + + private List buildPredicates(String q, Set queryFilters, + CriteriaBuilder cb, Path root) { + List finalPredicates = new ArrayList<>(); + + List stringFields = + schema.getSortedFields().stream().filter(f -> f.getType() == DbFieldType.STRING) + .collect(Collectors.toList()); + + List queryPredicates = new ArrayList<>(); + if (q != null) { + for (DbField f : stringFields) { + Path path = root.get(f.getJavaName()); + queryPredicates.add(cb.like(cb.lower(cb.toString(path)), "%" + q.toLowerCase() + "%")); + } + + Predicate queryPredicate = cb.or(queryPredicates.toArray(new Predicate[queryPredicates.size()])); + finalPredicates.add(queryPredicate); + } + + + if (queryFilters == null) queryFilters = new HashSet<>(); + for (QueryFilter filter : queryFilters) { + CompareOperator op = filter.getOp(); + String field = filter.getField(); + String v = filter.getValue(); + + DbField dbField = schema.getFieldByJavaName(field); + Object value = dbField.getType().parseValue(v); + + if (op == CompareOperator.STRING_EQ) { + finalPredicates.add(cb.equal(cb.lower(cb.toString(root.get(field))), value.toString().toLowerCase())); + } else if (op == CompareOperator.CONTAINS) { + finalPredicates.add( + cb.like(cb.lower(cb.toString(root.get(field))), "%" + value.toString().toLowerCase() + "%") + ); + } else if (op == CompareOperator.EQ) { + finalPredicates.add( + cb.equal(root.get(field), value) + ); + } else if (op == CompareOperator.GT) { + finalPredicates.add( + cb.greaterThan(root.get(field), value.toString()) + ); + } else if (op == CompareOperator.LT) { + finalPredicates.add( + cb.lessThan(root.get(field), value.toString()) + ); + } else if (op == CompareOperator.AFTER) { + if (value instanceof LocalDate) + finalPredicates.add( + cb.greaterThan(root.get(field), (LocalDate)value) + ); + else if (value instanceof LocalDateTime) + finalPredicates.add( + cb.greaterThan(root.get(field), (LocalDateTime)value) + ); + + } else if (op == CompareOperator.BEFORE) { + if (value instanceof LocalDate) + finalPredicates.add( + cb.lessThan(root.get(field), (LocalDate)value) + ); + else if (value instanceof LocalDateTime) + finalPredicates.add( + cb.lessThan(root.get(field), (LocalDateTime)value) + ); + + } + } + return finalPredicates; + } + + public void update(DbObjectSchema schema, Map params, Map files) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaUpdate update = cb.createCriteriaUpdate(schema.getJavaClass()); + + Root employee = update.from(schema.getJavaClass()); + + for (DbField field : schema.getSortedFields()) { + if (field.isPrimaryKey()) continue; + + String stringValue = params.get(field.getName()); + Object value = null; + if (stringValue != null && stringValue.isBlank()) stringValue = null; + if (stringValue != null) { + value = field.getType().parseValue(stringValue); + } else { + try { + MultipartFile file = files.get(field.getJavaName()); + if (file != null) + value = file.getBytes(); + } catch (IOException e) { + throw new DbAdminException(e); + } + } + + update.set(employee.get(field.getJavaName()), value); + } + String pkName = schema.getPrimaryKey().getJavaName(); + update.where(cb.equal(employee.get(pkName), params.get(schema.getPrimaryKey().getName()))); + + Query query = entityManager.createQuery(update); + int rowCount = query.executeUpdate(); + + } } diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java b/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java index 79e7ba3..d2716ee 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java @@ -3,10 +3,12 @@ package tech.ailef.dbadmin.dbmapping; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import org.springframework.data.domain.Page; @@ -21,6 +23,7 @@ import org.springframework.web.multipart.MultipartFile; import jakarta.transaction.Transactional; import tech.ailef.dbadmin.dto.PaginatedResult; import tech.ailef.dbadmin.dto.PaginationInfo; +import tech.ailef.dbadmin.dto.QueryFilter; import tech.ailef.dbadmin.exceptions.DbAdminException; import tech.ailef.dbadmin.exceptions.InvalidPageException; @@ -63,8 +66,8 @@ public class DbAdminRepository { * @param query * @return */ - public long count(DbObjectSchema schema, String query) { - return schema.getJpaRepository().count(query); + public long count(DbObjectSchema schema, String query, Set queryFilters) { + return schema.getJpaRepository().count(query, queryFilters); } @@ -116,7 +119,7 @@ public class DbAdminRepository { return new PaginatedResult( - new PaginationInfo(page, maxPage, pageSize, maxElement), + new PaginationInfo(page, maxPage, pageSize, maxElement, null, sortKey, sortOrder, new HashSet<>()), results ); } @@ -126,14 +129,18 @@ public class DbAdminRepository { * @param schema * @param params */ + @Transactional public void update(DbObjectSchema schema, Map params, Map files) { - Object[] updateArray = schema.getUpdateArray(params, files); +// Object[] updateArray = schema.getUpdateArray(params, files); +// +// String updateFields = +// schema.getSortedFields().stream().map(f -> "`" + f.getName() + "` = ?").collect(Collectors.joining(", ")); +// +// String query = "UPDATE `" + schema.getTableName() + "` SET " + updateFields + " WHERE `" + schema.getPrimaryKey().getName() + "` = ?"; +// jdbcTemplate.update(query, updateArray); - String updateFields = - schema.getSortedFields().stream().map(f -> "`" + f.getName() + "` = ?").collect(Collectors.joining(", ")); - - String query = "UPDATE `" + schema.getTableName() + "` SET " + updateFields + " WHERE `" + schema.getPrimaryKey().getName() + "` = ?"; - jdbcTemplate.update(query, updateArray); + schema.getJpaRepository().update(schema, params, files); + } @SuppressWarnings("unchecked") @@ -203,17 +210,6 @@ public class DbAdminRepository { insert.execute(allValues); return primaryKey; } -// String fieldsString = -// schema.getSortedFields().stream().skip(primaryKey == null ? 1 : 0).map(f -> "`" + f.getName() + "`").collect(Collectors.joining(", ")); -// -// String placeholdersString = -// schema.getSortedFields().stream().skip(primaryKey == null ? 1 : 0).map(f -> "?").collect(Collectors.joining(", ")); -// Object[] array = schema.getInsertArray(values, files); -// -// String query = "INSERT INTO " + schema.getTableName() + " (" + fieldsString + ") VALUES (" + placeholdersString + ");"; -// jdbcTemplate.update(query, array); - -// return primaryKey; } @@ -223,10 +219,11 @@ public class DbAdminRepository { * @param query * @return */ - public PaginatedResult search(DbObjectSchema schema, String query, int page, int pageSize, String sortKey, String sortOrder) { + public PaginatedResult search(DbObjectSchema schema, String query, int page, int pageSize, String sortKey, + String sortOrder, Set queryFilters) { AdvancedJpaRepository jpaRepository = schema.getJpaRepository(); - long maxElement = count(schema, query); + long maxElement = count(schema, query, queryFilters); int maxPage = (int)(Math.ceil ((double)maxElement / pageSize)); if (page <= 0) page = 1; @@ -235,8 +232,8 @@ public class DbAdminRepository { } return new PaginatedResult( - new PaginationInfo(page, maxPage, pageSize, maxElement), - jpaRepository.search(query, page, pageSize, sortKey, sortOrder).stream() + new PaginationInfo(page, maxPage, pageSize, maxElement, query, sortKey, sortOrder, queryFilters), + jpaRepository.search(query, page, pageSize, sortKey, sortOrder, queryFilters).stream() .map(o -> new DbObject(o, schema)) .toList() ); @@ -251,7 +248,7 @@ public class DbAdminRepository { public List search(DbObjectSchema schema, String query) { AdvancedJpaRepository jpaRepository = schema.getJpaRepository(); - return jpaRepository.search(query, 1, 50, null, null).stream() + return jpaRepository.search(query, 1, 50, null, null, null).stream() .map(o -> new DbObject(o, schema)) .toList(); } diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldType.java b/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldType.java index 871862e..389b5a2 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldType.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldType.java @@ -4,12 +4,14 @@ import java.io.IOException; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; import org.springframework.web.multipart.MultipartFile; import jakarta.persistence.ManyToMany; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; +import tech.ailef.dbadmin.dto.CompareOperator; import tech.ailef.dbadmin.exceptions.DbAdminException; public enum DbFieldType { @@ -29,6 +31,11 @@ public enum DbFieldType { public Class getJavaClass() { return Integer.class; } + + @Override + public List getCompareOperators() { + return List.of(CompareOperator.GT, CompareOperator.EQ, CompareOperator.LT); + } }, DOUBLE { @Override @@ -45,6 +52,11 @@ public enum DbFieldType { public Class getJavaClass() { return Double.class; } + + @Override + public List getCompareOperators() { + return List.of(CompareOperator.GT, CompareOperator.EQ, CompareOperator.LT); + } }, LONG { @Override @@ -61,6 +73,11 @@ public enum DbFieldType { public Class getJavaClass() { return Long.class; } + + @Override + public List getCompareOperators() { + return List.of(CompareOperator.GT, CompareOperator.EQ, CompareOperator.LT); + } }, FLOAT { @Override @@ -77,6 +94,11 @@ public enum DbFieldType { public Class getJavaClass() { return Float.class; } + + @Override + public List getCompareOperators() { + return List.of(CompareOperator.GT, CompareOperator.EQ, CompareOperator.LT); + } }, LOCAL_DATE { @Override @@ -93,6 +115,11 @@ public enum DbFieldType { public Class getJavaClass() { return Float.class; } + + @Override + public List getCompareOperators() { + return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE); + } }, LOCAL_DATE_TIME { @Override @@ -109,6 +136,11 @@ public enum DbFieldType { public Class getJavaClass() { return LocalDateTime.class; } + + @Override + public List getCompareOperators() { + return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE); + } }, STRING { @Override @@ -125,6 +157,11 @@ public enum DbFieldType { public Class getJavaClass() { return String.class; } + + @Override + public List getCompareOperators() { + return List.of(CompareOperator.CONTAINS, CompareOperator.STRING_EQ); + } }, BOOLEAN { @Override @@ -141,6 +178,11 @@ public enum DbFieldType { public Class getJavaClass() { return Boolean.class; } + + @Override + public List getCompareOperators() { + return List.of(CompareOperator.EQ); + } }, BIG_DECIMAL { @Override @@ -157,6 +199,11 @@ public enum DbFieldType { public Class getJavaClass() { return BigDecimal.class; } + + @Override + public List getCompareOperators() { + return List.of(CompareOperator.GT, CompareOperator.EQ, CompareOperator.LT); + } }, BYTE_ARRAY { @Override @@ -178,6 +225,10 @@ public enum DbFieldType { return byte[].class; } + @Override + public List getCompareOperators() { + throw new DbAdminException("Binary fields are not comparable"); + } }, ONE_TO_MANY { @Override @@ -204,6 +255,11 @@ public enum DbFieldType { public String toString() { return "One to Many"; } + + @Override + public List getCompareOperators() { + throw new DbAdminException(); + } }, ONE_TO_ONE { @Override @@ -230,6 +286,11 @@ public enum DbFieldType { public String toString() { return "One to One"; } + + @Override + public List getCompareOperators() { + throw new DbAdminException(); + } }, MANY_TO_MANY { @Override @@ -256,6 +317,11 @@ public enum DbFieldType { public String toString() { return "Many to Many"; } + + @Override + public List getCompareOperators() { + throw new DbAdminException(); + } }, COMPUTED { @Override @@ -272,6 +338,11 @@ public enum DbFieldType { public Class getJavaClass() { throw new UnsupportedOperationException(); } + + @Override + public List getCompareOperators() { + throw new DbAdminException(); + } }; public abstract String getHTMLName(); @@ -280,6 +351,8 @@ public enum DbFieldType { public abstract Class getJavaClass(); + public abstract List getCompareOperators(); + public boolean isRelationship() { return false; } diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldValue.java b/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldValue.java index a4e8a13..82d68a9 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldValue.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/DbFieldValue.java @@ -17,8 +17,13 @@ public class DbFieldValue { } public String getFormattedValue() { - if (field.getFormat() == null) return value == null ? "NULL" : value.toString(); - return String.format(field.getFormat(), value); + if (value == null) return null; + + if (field.getFormat() == null) { + return value.toString(); + } else { + return String.format(field.getFormat(), value); + } } public DbField getField() { diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/DbObject.java b/src/main/java/tech/ailef/dbadmin/dbmapping/DbObject.java index df7a82b..88ad372 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/DbObject.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/DbObject.java @@ -199,8 +199,13 @@ public class DbObject { String capitalize = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); Method[] methods = instance.getClass().getDeclaredMethods(); + String prefix = "get"; + if (schema.getFieldByJavaName(fieldName).getType() == DbFieldType.BOOLEAN) { + prefix = "is"; + } + for (Method m : methods) { - if (m.getName().equals("get" + capitalize)) + if (m.getName().equals(prefix + capitalize)) return m; } diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/DbObjectSchema.java b/src/main/java/tech/ailef/dbadmin/dbmapping/DbObjectSchema.java index 99c613f..9e50aff 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/DbObjectSchema.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/DbObjectSchema.java @@ -10,8 +10,6 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; -import org.springframework.web.multipart.MultipartFile; - import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.ManyToMany; @@ -19,6 +17,7 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import tech.ailef.dbadmin.DbAdmin; import tech.ailef.dbadmin.annotations.ComputedColumn; +import tech.ailef.dbadmin.annotations.Filterable; import tech.ailef.dbadmin.exceptions.DbAdminException; import tech.ailef.dbadmin.misc.Utils; @@ -173,98 +172,14 @@ public class DbObjectSchema { public Method getComputedColumn(String name) { return computedColumns.get(name); } - - public Object[] getInsertArray(Map params, Map files) { - int currentIndex = 0; - - String pkValue = params.get(getPrimaryKey().getName()); - if (pkValue == null || pkValue.isBlank()) - pkValue = null; - - Object[] row; - if (pkValue == null) { - row = new Object[getSortedFields().size() - 1]; - } else { - row = new Object[getSortedFields().size()]; - } - - for (DbField field : getSortedFields()) { - // Skip the primary key if the value is null - // If it is autogenerated, it will be filled by the database - // otherwise it will throw an error - if (field.isPrimaryKey() && pkValue == null) { - continue; - } - - String name = field.getName(); - - String stringValue = params.get(name); - Object value = null; - if (stringValue != null && stringValue.isBlank()) stringValue = null; - if (stringValue != null) { - value = stringValue; - } else { - value = files.get(name); - } - - String type = params.get("__dbadmin_" + name + "_type"); - - if (type == null) - throw new RuntimeException("Missing type hidden field for: " + name); - - try { - if (value == null) - row[currentIndex++] = null; - else - row[currentIndex++] = DbFieldType.valueOf(type).parseValue(value); - } catch (IllegalArgumentException | SecurityException e) { - e.printStackTrace(); - } - } - - return row; - } - public Object[] getUpdateArray(Map params, Map files) { - Object[] row = new Object[getSortedFields().size() + 1]; - - int currentIndex = 0; - DbField primaryKey = getPrimaryKey(); - String pkValue = params.get(primaryKey.getName()); - - for (DbField field : getSortedFields()) { - String name = field.getName(); - - String stringValue = params.get(name); - Object value = null; - if (stringValue != null && stringValue.isBlank()) stringValue = null; - if (stringValue != null) { - value = stringValue; - } else { - value = files.get(name); - } - - String type = params.get("__dbadmin_" + name + "_type"); - - if (type == null) - throw new RuntimeException("Missing type hidden field for: " + name); - - try { - if (value == null) - row[currentIndex++] = null; - else - row[currentIndex++] = DbFieldType.valueOf(type).parseValue(value); - } catch (IllegalArgumentException | SecurityException e) { - e.printStackTrace(); - } - } - - row[currentIndex] = primaryKey.getType().parseValue(pkValue); - - return row; + public List getFilterableFields() { + return getSortedFields().stream().filter(f -> { + return !f.isBinary() && !f.isPrimaryKey() + && f.getPrimitiveField().getAnnotation(Filterable.class) != null; + }).toList(); } - @Override public String toString() { return "DbObjectSchema [fields=" + fields + ", className=" + entityClass.getName() + "]"; diff --git a/src/main/java/tech/ailef/dbadmin/dto/CompareOperator.java b/src/main/java/tech/ailef/dbadmin/dto/CompareOperator.java new file mode 100644 index 0000000..6f850e7 --- /dev/null +++ b/src/main/java/tech/ailef/dbadmin/dto/CompareOperator.java @@ -0,0 +1,52 @@ +package tech.ailef.dbadmin.dto; + +public enum CompareOperator { + GT { + @Override + public String getDisplayName() { + return "Greater than"; + } + }, + LT { + @Override + public String getDisplayName() { + return "Less than"; + } + }, + EQ { + @Override + public String getDisplayName() { + return "Equals"; + } + }, + STRING_EQ { + @Override + public String getDisplayName() { + return "Equals"; + } + }, + BEFORE { + @Override + public String getDisplayName() { + return "Before"; + } + }, + AFTER { + @Override + public String getDisplayName() { + return "After"; + } + }, + CONTAINS { + @Override + public String getDisplayName() { + return "Contains"; + } + }; + + public abstract String getDisplayName(); + + public String toString() { + return this.name().toLowerCase(); + } +} diff --git a/src/main/java/tech/ailef/dbadmin/dto/ListModelRequest.java b/src/main/java/tech/ailef/dbadmin/dto/ListModelRequest.java new file mode 100644 index 0000000..7e9e82e --- /dev/null +++ b/src/main/java/tech/ailef/dbadmin/dto/ListModelRequest.java @@ -0,0 +1,37 @@ +//package tech.ailef.dbadmin.dto; +// +//import java.util.Set; +// +//public class ListModelRequest { +// private String className; +// +// private String query; +// +// private Integer page; +// +// private Integer pageSize; +// +// private String sortKey; +// +// private String sortOrder; +// +// private Set queryFilters; +// +// private PaginationInfo paginationInfo; +// +// public ListModelRequest(String className, String query, Integer page, Integer pageSize, String sortKey, +// String sortOrder, Set queryFilters, PaginationInfo paginationInfo) { +// super(); +// this.className = className; +// this.query = query; +// this.page = page; +// this.pageSize = pageSize; +// this.sortKey = sortKey; +// this.sortOrder = sortOrder; +// this.queryFilters = queryFilters; +// this.paginationInfo = paginationInfo; +// } +// +// +//// @RequestParam MultiValueMap otherParams, +//} diff --git a/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java b/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java index e41d8d0..dd69835 100644 --- a/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java +++ b/src/main/java/tech/ailef/dbadmin/dto/PaginationInfo.java @@ -1,9 +1,15 @@ package tech.ailef.dbadmin.dto; +import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.springframework.util.MultiValueMap; + +import tech.ailef.dbadmin.misc.Utils; + /** * Attached as output to requests that have a paginated response, * holds information about the current pagination. @@ -30,12 +36,25 @@ public class PaginationInfo { private int pageSize; private long maxElement; + + private Set queryFilters; + + private String query; + + private String sortKey; + + private String sortOrder; - public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement) { + public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement, String query, + String sortKey, String sortOrder, 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() { @@ -65,6 +84,36 @@ public class PaginationInfo { public long getMaxElement() { return maxElement; } + + public String getSortedPageLink(String sortKey, String sortOrder) { + MultiValueMap params = Utils.computeParams(queryFilters); + + if (query != null) { + params.put("query", new ArrayList<>()); + params.get("query").add(query); + } + + params.add("pageSize", "" + pageSize); + params.add("page", "" + currentPage); + params.add("sortKey", sortKey); + params.add("sortOrder", sortOrder); + + return Utils.getQueryString(params); + } + + public String getLink(int page) { + MultiValueMap params = Utils.computeParams(queryFilters); + + if (query != null) { + params.put("query", new ArrayList<>()); + params.get("query").add(query); + } + + params.add("pageSize", "" + pageSize); + params.add("page", "" + page); + + return Utils.getQueryString(params); + } public List getBeforePages() { return IntStream.range(Math.max(currentPage - PAGE_RANGE, 1), currentPage).boxed().collect(Collectors.toList()); diff --git a/src/main/java/tech/ailef/dbadmin/dto/QueryFilter.java b/src/main/java/tech/ailef/dbadmin/dto/QueryFilter.java new file mode 100644 index 0000000..2d52b3c --- /dev/null +++ b/src/main/java/tech/ailef/dbadmin/dto/QueryFilter.java @@ -0,0 +1,57 @@ +package tech.ailef.dbadmin.dto; + +import java.util.Objects; + +public class QueryFilter { + private String field; + + private CompareOperator op; + + private String value; + + public QueryFilter(String field, CompareOperator op, String value) { + this.field = field; + this.op = op; + this.value = value; + } + + public String getField() { + return field; + } + + public CompareOperator getOp() { + return op; + } + + public String getValue() { + return value; + } + + @Override + public int hashCode() { + return Objects.hash(field, op, value); + } + + @Override + public String toString() { + String displayValue = value; + if (value.length() > 10) { + displayValue = value.substring(0, 4) + "..." + value.substring(value.length() - 4); + } + return "'" + field + "' " + op.getDisplayName() + " '" + displayValue + "'"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + QueryFilter other = (QueryFilter) obj; + return Objects.equals(field, other.field) && Objects.equals(op, other.op) && Objects.equals(value, other.value); + } + + +} diff --git a/src/main/java/tech/ailef/dbadmin/misc/Utils.java b/src/main/java/tech/ailef/dbadmin/misc/Utils.java index 1667199..06f3632 100644 --- a/src/main/java/tech/ailef/dbadmin/misc/Utils.java +++ b/src/main/java/tech/ailef/dbadmin/misc/Utils.java @@ -1,5 +1,17 @@ package tech.ailef.dbadmin.misc; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import tech.ailef.dbadmin.dto.CompareOperator; +import tech.ailef.dbadmin.dto.QueryFilter; +import tech.ailef.dbadmin.exceptions.DbAdminException; + public interface Utils { public static String camelToSnake(String v) { if (Character.isUpperCase(v.charAt(0))) { @@ -10,6 +22,66 @@ public interface Utils { } + public static MultiValueMap computeParams(Set filters) { + MultiValueMap r = new LinkedMultiValueMap<>(); + + r.put("filter_field", new ArrayList<>()); + r.put("filter_op", new ArrayList<>()); + r.put("filter_value", new ArrayList<>()); + + for (QueryFilter filter : filters) { + r.get("filter_field").add(filter.getField()); + r.get("filter_op").add(filter.getOp().toString()); + r.get("filter_value").add(filter.getValue()); + } + + return r; + } + + public static Set computeFilters(MultiValueMap params) { + if (params == null) + return new HashSet<>(); + + List ops = params.get("filter_op"); + List fields = params.get("filter_field"); + List values = params.get("filter_value"); + + if (ops == null || fields == null || values == null) + return new HashSet<>(); + + if (ops.size() != fields.size() || fields.size() != values.size() + || ops.size() != values.size()) { + throw new DbAdminException("Filtering parameters must have the same size"); + } + + Set filters = new HashSet<>(); + for (int i = 0; i < ops.size(); i++) { + String op = ops.get(i); + String field = fields.get(i); + String value = values.get(i); + + QueryFilter queryFilter = new QueryFilter(field, CompareOperator.valueOf(op.toUpperCase()), value); + filters.add(queryFilter); + } + + return filters; + + } + + + public static String getQueryString(MultiValueMap params) { + Set currentParams = params.keySet(); + List paramValues = new ArrayList<>(); + for (String param : currentParams) { + for (String v : params.get(param)) { + paramValues.add(param + "=" + v.trim()); + } + } + + if (paramValues.isEmpty()) return ""; + return "?" + String.join("&", paramValues); + } + public static String snakeToCamel(String text) { boolean shouldConvertNextCharToLower = true; StringBuilder builder = new StringBuilder(); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5a8eb30..13b71fa 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ -spring.datasource.url=jdbc:h2:file:./dbadmin -spring.datasource.username=sa -spring.datasource.password=password +#spring.datasource.url=jdbc:h2:file:./database +#spring.datasource.username=sa +#spring.datasource.password=password #spring.h2.console.enabled=true - +#spring.jpa.show-sql=true diff --git a/src/main/resources/static/css/dbadmin.css b/src/main/resources/static/css/dbadmin.css index 9b546f2..2121c85 100644 --- a/src/main/resources/static/css/dbadmin.css +++ b/src/main/resources/static/css/dbadmin.css @@ -133,6 +133,17 @@ td.table-checkbox, th.table-checkbox { } +.cursor-pointer { + cursor: pointer; +} + + +.noselect { + cursor: pointer; -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + /** AUTOCOMPLETE **/ @@ -154,4 +165,12 @@ AUTOCOMPLETE .clear-all-badge { padding: 0.4rem; +} + +/** + * Filters + */ + +.filterable-field .card-header:hover { + background-color: #F0F0F0; } \ No newline at end of file diff --git a/src/main/resources/static/js/filters.js b/src/main/resources/static/js/filters.js new file mode 100644 index 0000000..befd667 --- /dev/null +++ b/src/main/resources/static/js/filters.js @@ -0,0 +1,34 @@ +document.addEventListener("DOMContentLoaded", () => { + let rootElements = document.querySelectorAll('.filterable-fields'); + + + rootElements.forEach(root => { + let fields = root.querySelectorAll('.filterable-field'); + + let activeFilters = root.querySelectorAll(".active-filter"); + activeFilters.forEach(activeFilter => { + activeFilter.addEventListener('click', function(e) { + let formId = e.target.dataset.formid; + document.getElementById(formId).submit() + }); + }); + + fields.forEach(field => { + field.querySelector(".card-header").addEventListener('click', function(e) { + if (field.querySelector(".card-body").classList.contains('d-none')) { + field.querySelector(".card-body").classList.remove('d-none'); + field.querySelector(".card-body").classList.add('d-block'); + field.querySelector(".card-header i.bi").classList.remove('bi-caret-right'); + field.querySelector(".card-header i.bi").classList.add('bi-caret-down'); + } else { + field.querySelector(".card-body").classList.remove('d-block'); + field.querySelector(".card-body").classList.add('d-none'); + field.querySelector(".card-header i.bi").classList.remove('bi-caret-down'); + field.querySelector(".card-header i.bi").classList.add('bi-caret-right'); + } + }); + }); + }); + + +}); \ No newline at end of file diff --git a/src/main/resources/templates/fragments/data_row.html b/src/main/resources/templates/fragments/data_row.html index 653f5da..f52960b 100644 --- a/src/main/resources/templates/fragments/data_row.html +++ b/src/main/resources/templates/fragments/data_row.html @@ -48,13 +48,24 @@ - - + + + + NULL + + + + + + - Download + Download + ([[ ${object.get(field).getValue().length} ]] bytes) + diff --git a/src/main/resources/templates/fragments/forms.html b/src/main/resources/templates/fragments/forms.html index 9eaca89..110203a 100644 --- a/src/main/resources/templates/fragments/forms.html +++ b/src/main/resources/templates/fragments/forms.html @@ -38,5 +38,73 @@ + + +
+
+ + +
+
+ +
+ + + + + + +
+ + + + Equals + +
+ + +
+
+
+ +
+ + + + + + + + + + + +
+
+ + +
+
diff --git a/src/main/resources/templates/fragments/resources.html b/src/main/resources/templates/fragments/resources.html index e9ee873..fbd511e 100644 --- a/src/main/resources/templates/fragments/resources.html +++ b/src/main/resources/templates/fragments/resources.html @@ -9,6 +9,7 @@ + @@ -124,14 +125,17 @@

Showing [[ ${page.getActualResults()} ]] of [[ ${page.getPagination().getMaxElement()} ]] results

@@ -180,7 +206,6 @@
-
diff --git a/src/main/resources/templates/fragments/table_selectable.html b/src/main/resources/templates/fragments/table_selectable.html index 26f18fc..8bd5262 100644 --- a/src/main/resources/templates/fragments/table_selectable.html +++ b/src/main/resources/templates/fragments/table_selectable.html @@ -27,21 +27,18 @@

- - + + - + + th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getSortedPageLink(field.getJavaName(), 'ASC')}|}"> + th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getSortedPageLink(field.getJavaName(), 'DESC')}|}"> diff --git a/src/main/resources/templates/model/create.html b/src/main/resources/templates/model/create.html index 832a340..bbb942d 100644 --- a/src/main/resources/templates/model/create.html +++ b/src/main/resources/templates/model/create.html @@ -39,7 +39,7 @@ : (object != null ? object.traverse(field).getPrimaryKeyValue() : '' ) })}">

- +
- + diff --git a/src/main/resources/templates/model/list.html b/src/main/resources/templates/model/list.html index d5e5c2b..d178812 100644 --- a/src/main/resources/templates/model/list.html +++ b/src/main/resources/templates/model/list.html @@ -15,7 +15,7 @@ [[ ${schema.getJavaClass().getSimpleName()} ]] From 9391db1ae996488bda44245b64573c99477ff356 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 20 Sep 2023 12:17:19 +0200 Subject: [PATCH 2/4] 0.0.2 README --- README.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3c4bd80..ee3f36a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Spring Boot Admin Panel +# 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. @@ -11,7 +11,7 @@ broken, please report it as an issue and I will try to take a look at it. ## Installation -1. Clone the Github repo and `mvn install` the project, then include the dependency in your `pom.xml`: +1. The code is not yet distributed on Maven, so for now you need to install manually. Clone the Github repo and `mvn install` the project, then include the dependency in your `pom.xml`: ``` @@ -50,4 +50,56 @@ The last step is to annotate your `@SpringBootApplication` class containing the 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. -3. At this point, when you run your application, you should be able to visit `http://localhost:$PORT/dbadmin` and access the web interface. \ No newline at end of file +3. At this point, when you run your application, you should be able to visit `http://localhost:$PORT/dbadmin` 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. +The following annotations are supported. + +### @DisplayName +``` + @DisplayName + public String getName() { + return name; + } +``` + +To show an item in a table its primary key is used by default. If you set a method as `@DisplayName` in your `@Entity` class, this result will be shown in addition to its primary key wherever possible. + +### @DisplayFormat +``` + @DisplayFormat(format = "$%.2f") + private Double price; +``` + +Specify a format to apply when displaying the field. + +### @ComputedColumn +``` + @ComputedColumn + public double totalSpent() { + double total = 0; + for (Order o : orders) { + total += o.total(); + } + return total; + } +``` + +Add an extra field that's computed at runtime instead of a database column. It will be displayed everywhere as a normal, read-only column. + +### @Filterable + +``` + @Filterable + private LocalDate createdAt; +``` + +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 this table. + + +## Changelog + +0.0.2 - Faceted search with `@Filterable` annotation +0.0.1 - First alpha release (basic CRUD features) From 06d59160212923936fccc73cf976d2437b3b4b1d Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 20 Sep 2023 12:17:51 +0200 Subject: [PATCH 3/4] 0.0.2 README --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ee3f36a..7f7d26e 100644 --- a/README.md +++ b/README.md @@ -59,32 +59,32 @@ The following annotations are supported. ### @DisplayName ``` - @DisplayName - public String getName() { - return name; - } +@DisplayName +public String getName() { + return name; +} ``` To show an item in a table its primary key is used by default. If you set a method as `@DisplayName` in your `@Entity` class, this result will be shown in addition to its primary key wherever possible. ### @DisplayFormat ``` - @DisplayFormat(format = "$%.2f") - private Double price; +@DisplayFormat(format = "$%.2f") +private Double price; ``` Specify a format to apply when displaying the field. ### @ComputedColumn ``` - @ComputedColumn - public double totalSpent() { - double total = 0; - for (Order o : orders) { - total += o.total(); - } - return total; +@ComputedColumn +public double totalSpent() { + double total = 0; + for (Order o : orders) { + total += o.total(); } + return total; +} ``` Add an extra field that's computed at runtime instead of a database column. It will be displayed everywhere as a normal, read-only column. @@ -92,8 +92,8 @@ Add an extra field that's computed at runtime instead of a database column. It w ### @Filterable ``` - @Filterable - private LocalDate createdAt; +@Filterable +private LocalDate createdAt; ``` 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 this table. From 8039801940b3746d64f4390f730a236f5682b893 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 20 Sep 2023 12:22:22 +0200 Subject: [PATCH 4/4] 0.0.2 README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7f7d26e..27d99cc 100644 --- a/README.md +++ b/README.md @@ -102,4 +102,5 @@ Place on one or more fields in a class to activate the faceted search feature. T ## Changelog 0.0.2 - Faceted search with `@Filterable` annotation + 0.0.1 - First alpha release (basic CRUD features)