This commit is contained in:
Francesco 2023-09-19 18:02:40 +02:00
parent 2db84d9996
commit 234f3d94c8
12 changed files with 392 additions and 77 deletions

View File

@ -2,9 +2,11 @@ package tech.ailef.dbadmin.controller;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -12,6 +14,7 @@ import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -22,14 +25,18 @@ import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.persistence.criteria.Predicate; import ch.qos.logback.core.joran.action.ParamAction;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import tech.ailef.dbadmin.DbAdmin; import tech.ailef.dbadmin.DbAdmin;
import tech.ailef.dbadmin.dbmapping.DbAdminRepository; import tech.ailef.dbadmin.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.dbmapping.DbObject; import tech.ailef.dbadmin.dbmapping.DbObject;
import tech.ailef.dbadmin.dbmapping.DbObjectSchema; import tech.ailef.dbadmin.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.dto.PaginatedResult; import tech.ailef.dbadmin.dto.PaginatedResult;
import tech.ailef.dbadmin.dto.QueryFilter;
import tech.ailef.dbadmin.exceptions.DbAdminException; import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.exceptions.InvalidPageException; import tech.ailef.dbadmin.exceptions.InvalidPageException;
import tech.ailef.dbadmin.misc.Utils;
@Controller @Controller
@RequestMapping("/dbadmin") @RequestMapping("/dbadmin")
@ -92,18 +99,54 @@ public class DefaultDbAdminController {
public String list(Model model, @PathVariable String className, public String list(Model model, @PathVariable String className,
@RequestParam(required=false) Integer page, @RequestParam(required=false) String query, @RequestParam(required=false) Integer page, @RequestParam(required=false) String query,
@RequestParam(required=false) Integer pageSize, @RequestParam(required=false) String sortKey, @RequestParam(required=false) Integer pageSize, @RequestParam(required=false) String sortKey,
@RequestParam(required=false) String sortOrder, @RequestParam MultiValueMap<String, String> otherParams) { @RequestParam(required=false) String sortOrder, @RequestParam MultiValueMap<String, String> otherParams,
System.out.println(otherParams); HttpServletRequest request,
HttpServletResponse response) {
if (page == null) page = 1; if (page == null) page = 1;
if (pageSize == null) pageSize = 50; if (pageSize == null) pageSize = 50;
Set<QueryFilter> queryFilters = Utils.computeFilters(otherParams);
if (otherParams.containsKey("remove_field")) {
List<String> fields = otherParams.get("remove_field");
for (int i = 0; i < fields.size(); i++) {
QueryFilter toRemove =
new QueryFilter(fields.get(i), otherParams.get("remove_op").get(i), otherParams.get("remove_value").get(i));
queryFilters.removeIf(f -> f.equals(toRemove));
}
MultiValueMap<String, String> parameterMap = Utils.computeParams(queryFilters);
MultiValueMap<String, String> 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); DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
try { try {
PaginatedResult result = null; PaginatedResult result = null;
if (query != null || !otherParams.isEmpty()) { if (query != null || !otherParams.isEmpty()) {
result = repository.search(schema, query, page, pageSize, sortKey, sortOrder, otherParams); result = repository.search(schema, query, page, pageSize, sortKey, sortOrder, queryFilters);
} else { } else {
result = repository.findAll(schema, page, pageSize, sortKey, sortOrder); result = repository.findAll(schema, page, pageSize, sortKey, sortOrder);
} }
@ -115,6 +158,7 @@ public class DefaultDbAdminController {
model.addAttribute("sortKey", sortKey); model.addAttribute("sortKey", sortKey);
model.addAttribute("query", query); model.addAttribute("query", query);
model.addAttribute("sortOrder", sortOrder); model.addAttribute("sortOrder", sortOrder);
model.addAttribute("activeFilters", queryFilters);
return "model/list"; return "model/list";
} catch (InvalidPageException e) { } catch (InvalidPageException e) {

View File

@ -2,10 +2,10 @@ package tech.ailef.dbadmin.dbmapping;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.util.MultiValueMap;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaBuilder;
@ -13,7 +13,7 @@ import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Root;
import tech.ailef.dbadmin.exceptions.DbAdminException; import tech.ailef.dbadmin.dto.QueryFilter;
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public class AdvancedJpaRepository extends SimpleJpaRepository { public class AdvancedJpaRepository extends SimpleJpaRepository {
@ -30,12 +30,12 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public long count(String q, MultiValueMap<String, String> filteringParams) { public long count(String q, Set<QueryFilter> queryFilters) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(Long.class); CriteriaQuery query = cb.createQuery(Long.class);
Root root = query.from(schema.getJavaClass()); Root root = query.from(schema.getJavaClass());
List<Predicate> finalPredicates = buildPredicates(q, filteringParams, cb, root); List<Predicate> finalPredicates = buildPredicates(q, queryFilters, cb, root);
query.select(cb.count(root.get(schema.getPrimaryKey().getName()))) query.select(cb.count(root.get(schema.getPrimaryKey().getName())))
.where( .where(
@ -49,12 +49,12 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public List<Object> search(String q, int page, int pageSize, String sortKey, String sortOrder, MultiValueMap<String, String> filteringParams) { public List<Object> search(String q, int page, int pageSize, String sortKey, String sortOrder, Set<QueryFilter> filters) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(schema.getJavaClass()); CriteriaQuery query = cb.createQuery(schema.getJavaClass());
Root root = query.from(schema.getJavaClass()); Root root = query.from(schema.getJavaClass());
List<Predicate> finalPredicates = buildPredicates(q, filteringParams, cb, root); List<Predicate> finalPredicates = buildPredicates(q, filters, cb, root);
query.select(root) query.select(root)
.where( .where(
@ -70,7 +70,7 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
.setFirstResult((page - 1) * pageSize).getResultList(); .setFirstResult((page - 1) * pageSize).getResultList();
} }
private List<Predicate> buildPredicates(String q, MultiValueMap<String, String> filteringParams, private List<Predicate> buildPredicates(String q, Set<QueryFilter> queryFilters,
CriteriaBuilder cb, Path root) { CriteriaBuilder cb, Path root) {
List<Predicate> finalPredicates = new ArrayList<>(); List<Predicate> finalPredicates = new ArrayList<>();
@ -89,33 +89,29 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
finalPredicates.add(queryPredicate); finalPredicates.add(queryPredicate);
} }
/* for (QueryFilter filter : queryFilters) {
* Compute filtering predicates String op = filter.getOp();
*/ String field = filter.getField();
if (filteringParams != null) { String value = filter.getValue();
List<String> ops = filteringParams.get("filter_op[]");
List<String> fields = filteringParams.get("filter_field[]");
List<String> values = filteringParams.get("filter_value[]");
if (ops != null && fields != null && values != null) {
if (ops.size() != fields.size() || fields.size() != values.size()
|| ops.size() != values.size()) {
throw new DbAdminException("Filtering parameters must have the same size");
}
for (int i = 0; i < ops.size(); i++) {
String op = ops.get(i);
String field = fields.get(i);
String value = values.get(i);
if (op.equalsIgnoreCase("equals")) { if (op.equalsIgnoreCase("equals")) {
finalPredicates.add(cb.equal(cb.toString(root.get(field)), value)); finalPredicates.add(cb.equal(cb.lower(cb.toString(root.get(field))), value.toLowerCase()));
} else if (op.equalsIgnoreCase("contains")) { } else if (op.equalsIgnoreCase("contains")) {
System.out.println("CONTAINS"); finalPredicates.add(
finalPredicates.add(cb.like(cb.toString(root.get(field)), "%" + value + "%")); cb.like(cb.lower(cb.toString(root.get(field))), "%" + value.toLowerCase() + "%")
} );
} } else if (op.equalsIgnoreCase("eq")) {
finalPredicates.add(
cb.equal(root.get(field), value)
);
} else if (op.equalsIgnoreCase("gt")) {
finalPredicates.add(
cb.greaterThan(root.get(field), value)
);
} else if (op.equalsIgnoreCase("lt")) {
finalPredicates.add(
cb.lessThan(root.get(field), value)
);
} }
} }
return finalPredicates; return finalPredicates;

View File

@ -3,10 +3,12 @@ package tech.ailef.dbadmin.dbmapping;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@ -16,12 +18,12 @@ import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import tech.ailef.dbadmin.dto.PaginatedResult; import tech.ailef.dbadmin.dto.PaginatedResult;
import tech.ailef.dbadmin.dto.PaginationInfo; import tech.ailef.dbadmin.dto.PaginationInfo;
import tech.ailef.dbadmin.dto.QueryFilter;
import tech.ailef.dbadmin.exceptions.DbAdminException; import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.exceptions.InvalidPageException; import tech.ailef.dbadmin.exceptions.InvalidPageException;
@ -64,8 +66,8 @@ public class DbAdminRepository {
* @param query * @param query
* @return * @return
*/ */
public long count(DbObjectSchema schema, String query, MultiValueMap<String, String> filteringParams) { public long count(DbObjectSchema schema, String query, Set<QueryFilter> queryFilters) {
return schema.getJpaRepository().count(query, filteringParams); return schema.getJpaRepository().count(query, queryFilters);
} }
@ -117,7 +119,7 @@ public class DbAdminRepository {
return new PaginatedResult( return new PaginatedResult(
new PaginationInfo(page, maxPage, pageSize, maxElement), new PaginationInfo(page, maxPage, pageSize, maxElement, null, new HashSet<>()),
results results
); );
} }
@ -225,10 +227,10 @@ public class DbAdminRepository {
* @return * @return
*/ */
public PaginatedResult search(DbObjectSchema schema, String query, int page, int pageSize, String sortKey, public PaginatedResult search(DbObjectSchema schema, String query, int page, int pageSize, String sortKey,
String sortOrder, MultiValueMap<String, String> filteringParams) { String sortOrder, Set<QueryFilter> queryFilters) {
AdvancedJpaRepository jpaRepository = schema.getJpaRepository(); AdvancedJpaRepository jpaRepository = schema.getJpaRepository();
long maxElement = count(schema, query, filteringParams); long maxElement = count(schema, query, queryFilters);
int maxPage = (int)(Math.ceil ((double)maxElement / pageSize)); int maxPage = (int)(Math.ceil ((double)maxElement / pageSize));
if (page <= 0) page = 1; if (page <= 0) page = 1;
@ -237,8 +239,8 @@ public class DbAdminRepository {
} }
return new PaginatedResult( return new PaginatedResult(
new PaginationInfo(page, maxPage, pageSize, maxElement), new PaginationInfo(page, maxPage, pageSize, maxElement, query, queryFilters),
jpaRepository.search(query, page, pageSize, sortKey, sortOrder, filteringParams).stream() jpaRepository.search(query, page, pageSize, sortKey, sortOrder, queryFilters).stream()
.map(o -> new DbObject(o, schema)) .map(o -> new DbObject(o, schema))
.toList() .toList()
); );

View File

@ -4,6 +4,7 @@ import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -29,6 +30,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() { public Class<?> getJavaClass() {
return Integer.class; return Integer.class;
} }
@Override
public List<String> getCompareOperators() {
return List.of("gt", "eq", "lt");
}
}, },
DOUBLE { DOUBLE {
@Override @Override
@ -45,6 +51,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() { public Class<?> getJavaClass() {
return Double.class; return Double.class;
} }
@Override
public List<String> getCompareOperators() {
return List.of("gt", "eq", "lt");
}
}, },
LONG { LONG {
@Override @Override
@ -61,6 +72,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() { public Class<?> getJavaClass() {
return Long.class; return Long.class;
} }
@Override
public List<String> getCompareOperators() {
return List.of("gt", "eq", "lt");
}
}, },
FLOAT { FLOAT {
@Override @Override
@ -77,6 +93,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() { public Class<?> getJavaClass() {
return Float.class; return Float.class;
} }
@Override
public List<String> getCompareOperators() {
return List.of("gt", "eq", "lt");
}
}, },
LOCAL_DATE { LOCAL_DATE {
@Override @Override
@ -93,6 +114,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() { public Class<?> getJavaClass() {
return Float.class; return Float.class;
} }
@Override
public List<String> getCompareOperators() {
return List.of("After", "Equals", "Before");
}
}, },
LOCAL_DATE_TIME { LOCAL_DATE_TIME {
@Override @Override
@ -109,6 +135,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() { public Class<?> getJavaClass() {
return LocalDateTime.class; return LocalDateTime.class;
} }
@Override
public List<String> getCompareOperators() {
return List.of("After", "Equals", "Before");
}
}, },
STRING { STRING {
@Override @Override
@ -125,6 +156,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() { public Class<?> getJavaClass() {
return String.class; return String.class;
} }
@Override
public List<String> getCompareOperators() {
return List.of("Equals", "Contains");
}
}, },
BOOLEAN { BOOLEAN {
@Override @Override
@ -141,6 +177,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() { public Class<?> getJavaClass() {
return Boolean.class; return Boolean.class;
} }
@Override
public List<String> getCompareOperators() {
return List.of("Equals");
}
}, },
BIG_DECIMAL { BIG_DECIMAL {
@Override @Override
@ -157,6 +198,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() { public Class<?> getJavaClass() {
return BigDecimal.class; return BigDecimal.class;
} }
@Override
public List<String> getCompareOperators() {
return List.of("gt", "eq", "lt");
}
}, },
BYTE_ARRAY { BYTE_ARRAY {
@Override @Override
@ -178,6 +224,10 @@ public enum DbFieldType {
return byte[].class; return byte[].class;
} }
@Override
public List<String> getCompareOperators() {
return List.of("Equals");
}
}, },
ONE_TO_MANY { ONE_TO_MANY {
@Override @Override
@ -204,6 +254,11 @@ public enum DbFieldType {
public String toString() { public String toString() {
return "One to Many"; return "One to Many";
} }
@Override
public List<String> getCompareOperators() {
throw new DbAdminException();
}
}, },
ONE_TO_ONE { ONE_TO_ONE {
@Override @Override
@ -230,6 +285,11 @@ public enum DbFieldType {
public String toString() { public String toString() {
return "One to One"; return "One to One";
} }
@Override
public List<String> getCompareOperators() {
throw new DbAdminException();
}
}, },
MANY_TO_MANY { MANY_TO_MANY {
@Override @Override
@ -256,6 +316,11 @@ public enum DbFieldType {
public String toString() { public String toString() {
return "Many to Many"; return "Many to Many";
} }
@Override
public List<String> getCompareOperators() {
throw new DbAdminException();
}
}, },
COMPUTED { COMPUTED {
@Override @Override
@ -272,6 +337,11 @@ public enum DbFieldType {
public Class<?> getJavaClass() { public Class<?> getJavaClass() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public List<String> getCompareOperators() {
throw new DbAdminException();
}
}; };
public abstract String getHTMLName(); public abstract String getHTMLName();
@ -280,6 +350,8 @@ public enum DbFieldType {
public abstract Class<?> getJavaClass(); public abstract Class<?> getJavaClass();
public abstract List<String> getCompareOperators();
public boolean isRelationship() { public boolean isRelationship() {
return false; return false;
} }

View File

@ -1,9 +1,15 @@
package tech.ailef.dbadmin.dto; package tech.ailef.dbadmin.dto;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; 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, * Attached as output to requests that have a paginated response,
* holds information about the current pagination. * holds information about the current pagination.
@ -31,11 +37,17 @@ public class PaginationInfo {
private long maxElement; private long maxElement;
public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement) { private Set<QueryFilter> queryFilters;
private String query;
public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement, String query, Set<QueryFilter> queryFilters) {
this.currentPage = currentPage; this.currentPage = currentPage;
this.maxPage = maxPage; this.maxPage = maxPage;
this.pageSize = pageSize; this.pageSize = pageSize;
this.query = query;
this.maxElement = maxElement; this.maxElement = maxElement;
this.queryFilters = queryFilters;
} }
public int getCurrentPage() { public int getCurrentPage() {
@ -66,6 +78,20 @@ public class PaginationInfo {
return maxElement; return maxElement;
} }
public String getLink(int page) {
MultiValueMap<String, String> 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<Integer> getBeforePages() { public List<Integer> getBeforePages() {
return IntStream.range(Math.max(currentPage - PAGE_RANGE, 1), currentPage).boxed().collect(Collectors.toList()); return IntStream.range(Math.max(currentPage - PAGE_RANGE, 1), currentPage).boxed().collect(Collectors.toList());
} }

View File

@ -0,0 +1,53 @@
package tech.ailef.dbadmin.dto;
import java.util.Objects;
public class QueryFilter {
private String field;
private String op;
private String value;
public QueryFilter(String field, String op, String value) {
this.field = field;
this.op = op;
this.value = value;
}
public String getField() {
return field;
}
public String getOp() {
return op;
}
public String getValue() {
return value;
}
@Override
public int hashCode() {
return Objects.hash(field, op, value);
}
@Override
public String toString() {
return field + " " + op + " '" + value + "'";
}
@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);
}
}

View File

@ -1,5 +1,16 @@
package tech.ailef.dbadmin.misc; 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.QueryFilter;
import tech.ailef.dbadmin.exceptions.DbAdminException;
public interface Utils { public interface Utils {
public static String camelToSnake(String v) { public static String camelToSnake(String v) {
if (Character.isUpperCase(v.charAt(0))) { if (Character.isUpperCase(v.charAt(0))) {
@ -10,6 +21,66 @@ public interface Utils {
} }
public static MultiValueMap<String, String> computeParams(Set<QueryFilter> filters) {
MultiValueMap<String, String> 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());
r.get("filter_value").add(filter.getValue());
}
return r;
}
public static Set<QueryFilter> computeFilters(MultiValueMap<String, String> params) {
if (params == null)
return new HashSet<>();
List<String> ops = params.get("filter_op");
List<String> fields = params.get("filter_field");
List<String> 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<QueryFilter> 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, op, value);
filters.add(queryFilter);
}
return filters;
}
public static String getQueryString(MultiValueMap<String, String> params) {
Set<String> currentParams = params.keySet();
List<String> 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) { public static String snakeToCamel(String text) {
boolean shouldConvertNextCharToLower = true; boolean shouldConvertNextCharToLower = true;
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();

View File

@ -6,3 +6,4 @@ spring.datasource.password=password
spring.jpa.show-sql=true spring.jpa.show-sql=true
server.tomcat.relaxed-path-chars=[,]

View File

@ -1,19 +1,34 @@
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
let rootElements = document.querySelectorAll('.filterable-field'); let rootElements = document.querySelectorAll('.filterable-fields');
rootElements.forEach(root => { rootElements.forEach(root => {
root.querySelector(".card-header").addEventListener('click', function(e) { let fields = root.querySelectorAll('.filterable-field');
if (root.querySelector(".card-body").classList.contains('d-none')) {
root.querySelector(".card-body").classList.remove('d-none'); let activeFilters = root.querySelectorAll(".active-filter");
root.querySelector(".card-body").classList.add('d-block'); activeFilters.forEach(activeFilter => {
root.querySelector(".card-header i.bi").classList.remove('bi-caret-right'); activeFilter.addEventListener('click', function(e) {
root.querySelector(".card-header i.bi").classList.add('bi-caret-down'); 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 { } else {
root.querySelector(".card-body").classList.remove('d-block'); field.querySelector(".card-body").classList.remove('d-block');
root.querySelector(".card-body").classList.add('d-none'); field.querySelector(".card-body").classList.add('d-none');
root.querySelector(".card-header i.bi").classList.remove('bi-caret-down'); field.querySelector(".card-header i.bi").classList.remove('bi-caret-down');
root.querySelector(".card-header i.bi").classList.add('bi-caret-right'); field.querySelector(".card-header i.bi").classList.add('bi-caret-right');
} }
}); });
}); });
}); });
});

View File

@ -48,6 +48,10 @@
<div class="card-body d-none"> <div class="card-body d-none">
<form action="" method="GET"> <form action="" method="GET">
<!-- Reset page when applying filter to start back at page 1 -->
<!-- <input type="hidden" name="page" value="1">
<input type="hidden" name="pageSize" value="50"> -->
<th:block th:each="p : ${queryParams.keySet()}"> <th:block th:each="p : ${queryParams.keySet()}">
<input th:each="v : ${queryParams.get(p)}" th:name="${p}" th:value="${v}" type="hidden"> <input th:each="v : ${queryParams.get(p)}" th:name="${p}" th:value="${v}" type="hidden">
</th:block> </th:block>
@ -59,14 +63,13 @@
</th:block> </th:block>
<th:block th:unless="${field.isForeignKey()}"> <th:block th:unless="${field.isForeignKey()}">
<div class="container w-25"> <div class="container w-25">
<select class="form-select w-auto" name="filter_op[]"> <select class="form-select w-auto" name="filter_op">
<option value="equals">Equals</option> <option th:value="${op}" th:each="op : ${field.getType().getCompareOperators()}" th:text="${op}">
<option value="contains">Contains</option>
</select> </select>
</div> </div>
<input type="hidden" name="filter_field[]" th:value="${field.getName()}"> <input type="hidden" name="filter_field" th:value="${field.getJavaName()}">
<input placeholder="NULL" th:type="${field.getType().getHTMLName()}" <input placeholder="NULL" th:type="${field.getType().getHTMLName()}"
name="filter_value[]" name="filter_value"
class="form-control" th:id="|__id_${field.getName()}|" class="form-control" th:id="|__id_${field.getName()}|"
th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}" th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}"
th:required="${!field.isNullable() && !field.isPrimaryKey()}" th:required="${!field.isNullable() && !field.isPrimaryKey()}"

View File

@ -125,14 +125,17 @@
<div th:if="${page != null && page.getPagination().getMaxPage() != 1}" class="d-flex"> <div th:if="${page != null && page.getPagination().getMaxPage() != 1}" class="d-flex">
<ul class="pagination me-3"> <ul class="pagination me-3">
<li class="page-item" th:if="${page.getPagination().getCurrentPage() != 1}"> <li class="page-item" th:if="${page.getPagination().getCurrentPage() != 1}">
<a class="page-link" th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${page.getPagination().getCurrentPage() - 1},pageSize=${page.getPagination().getPageSize()})}" aria-label="Previous"> <a class="page-link"
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(page.getPagination.getCurrentPage() - 1)}|}"
aria-label="Previous">
<span aria-hidden="true">&laquo;</span> <span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span> <span class="sr-only">Previous</span>
</a> </a>
</li> </li>
<li class="page-item" th:each="p : ${page.getPagination().getBeforePages()}"> <li class="page-item" th:each="p : ${page.getPagination().getBeforePages()}">
<a class="page-link" th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${p},pageSize=${page.getPagination().getPageSize()})}" th:text="${p}"></a> <a class="page-link"
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(p)}|}" th:text="${p}"></a>
</li> </li>
<li class="page-item active"> <li class="page-item active">
@ -140,12 +143,15 @@
</li> </li>
<li class="page-item" th:each="p : ${page.getPagination().getAfterPages()}"> <li class="page-item" th:each="p : ${page.getPagination().getAfterPages()}">
<a class="page-link" th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${p},pageSize=${page.getPagination().getPageSize()})}" th:text="${p}"></a> <a class="page-link"
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(p)}|}"
th:text="${p}"></a>
</li> </li>
<li class="page-item"> <li class="page-item">
<a class="page-link" <a class="page-link"
th:if="${!page.getPagination().isLastPage()}" th:if="${!page.getPagination().isLastPage()}"
th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${page.getPagination().getCurrentPage() + 1},pageSize=${page.getPagination().getPageSize()})}" aria-label="Next"> th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(page.getPagination.getCurrentPage() + 1)}|}"
aria-label="Next">
<span class="sr-only">Next</span> <span class="sr-only">Next</span>
<span aria-hidden="true">&raquo;</span> <span aria-hidden="true">&raquo;</span>
</a> </a>

View File

@ -39,6 +39,9 @@
class="ui-text-input form-control" name="query" autofocus> class="ui-text-input form-control" name="query" autofocus>
<button class="ui-btn btn btn-primary">Search</button> <button class="ui-btn btn btn-primary">Search</button>
</div> </div>
<th:block th:each="p : ${queryParams.keySet()}">
<input th:if="${!p.equals('query')}" th:each="v : ${queryParams.get(p)}" th:name="${p}" th:value="${v}" type="hidden">
</th:block>
</form> </form>
<div class="separator mb-4 mt-4"></div> <div class="separator mb-4 mt-4"></div>
@ -50,11 +53,10 @@
<span title="Database table name" class="ms-3 label label-primary label-gray font-monospace"> <span title="Database table name" class="ms-3 label label-primary label-gray font-monospace">
[[ ${schema.getTableName()} ]] [[ ${schema.getTableName()} ]]
</span> </span>
</h3> </h3>
<h3><a title="Create new item"
<h3>
<a title="Create new item"
th:href="|/dbadmin/model/${schema.getClassName()}/create|"><i class="bi bi-plus-square"></i></a> th:href="|/dbadmin/model/${schema.getClassName()}/create|"><i class="bi bi-plus-square"></i></a>
</h3> </h3>
</div> </div>
@ -64,10 +66,34 @@
</div> </div>
</div> </div>
<div th:if="${!schema.getFilterableFields().isEmpty()}" class="col-3"> <div th:if="${!schema.getFilterableFields().isEmpty()}" class="col-3">
<div class="box"> <div class="box filterable-fields">
<h3 class="fw-bold mb-3"><i class="bi bi-funnel"></i> Filters</h3> <h3 class="fw-bold mb-3"><i class="bi bi-funnel"></i> Filters</h3>
<div class="mb-2">
<div th:each="filter : ${activeFilters}">
<span title="Click to remove this filter"
class="active-filter badge bg-primary me-1 mb-2 p-2 font-monospace cursor-pointer noselect"
th:data-formid="${filter.toString()}"
th:text="${filter}">
</span>
<form action="" th:id="${filter.toString()}" method="GET">
<th:block th:each="p : ${queryParams.keySet()}">
<input th:each="v : ${queryParams.get(p)}" th:name="${p}" th:value="${v}" type="hidden">
</th:block>
<input type="hidden" name="remove_field" th:value="${filter.getField()}">
<input type="hidden" name="remove_op" th:value="${filter.getOp()}">
<input type="hidden" name="remove_value" th:value="${filter.getValue()}">
</form>
</div>
</div>
<th:block th:each="field : ${schema.getFilterableFields()}"> <th:block th:each="field : ${schema.getFilterableFields()}">
<div th:replace="~{fragments/forms :: filter_field(field=${field})}"></div> <div th:replace="~{fragments/forms :: filter_field(field=${field})}"></div>