mirror of
https://github.com/dalbodeule/snap-admin.git
synced 2025-06-08 21:38:21 +00:00
0.0.2
This commit is contained in:
parent
c0801cc69d
commit
774c862ab3
2
pom.xml
2
pom.xml
@ -11,7 +11,7 @@
|
||||
</parent>
|
||||
<groupId>tech.ailef</groupId>
|
||||
<artifactId>spring-boot-db-admin</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<version>0.0.2</version>
|
||||
<name>spring-boot-db-admin</name>
|
||||
<description>Srping Boot DB Admin Dashboard</description>
|
||||
<properties>
|
||||
|
@ -95,7 +95,6 @@ public class DbAdmin {
|
||||
field.setSchema(schema);
|
||||
|
||||
schema.addField(field);
|
||||
System.out.println(field);
|
||||
}
|
||||
|
||||
return schema;
|
||||
|
11
src/main/java/tech/ailef/dbadmin/annotations/Filterable.java
Normal file
11
src/main/java/tech/ailef/dbadmin/annotations/Filterable.java
Normal file
@ -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 {
|
||||
}
|
@ -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<String, String> otherParams,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
|
||||
if (page == null) page = 1;
|
||||
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),
|
||||
CompareOperator.valueOf(otherParams.get("remove_op").get(i).toUpperCase()),
|
||||
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);
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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<String, String[]> getQueryParams(HttpServletRequest request) {
|
||||
return request.getParameterMap();
|
||||
}
|
||||
}
|
@ -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<QueryFilter> queryFilters) {
|
||||
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
CriteriaQuery query = cb.createQuery(Long.class);
|
||||
Root root = query.from(schema.getJavaClass());
|
||||
|
||||
List<DbField> stringFields =
|
||||
schema.getSortedFields().stream().filter(f -> f.getType() == DbFieldType.STRING)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
System.out.println("STRING F = " + stringFields);
|
||||
List<Predicate> 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<Predicate> 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<Object> search(String q, int page, int pageSize, String sortKey, String sortOrder) {
|
||||
public List<Object> search(String q, int page, int pageSize, String sortKey, String sortOrder, Set<QueryFilter> filters) {
|
||||
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
CriteriaQuery query = cb.createQuery(schema.getJavaClass());
|
||||
Root root = query.from(schema.getJavaClass());
|
||||
|
||||
List<DbField> stringFields =
|
||||
schema.getSortedFields().stream().filter(f -> f.getType() == DbFieldType.STRING)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Predicate> 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<Predicate> finalPredicates = buildPredicates(q, filters, cb, root);
|
||||
|
||||
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<Predicate> buildPredicates(String q, Set<QueryFilter> queryFilters,
|
||||
CriteriaBuilder cb, Path root) {
|
||||
List<Predicate> finalPredicates = new ArrayList<>();
|
||||
|
||||
List<DbField> stringFields =
|
||||
schema.getSortedFields().stream().filter(f -> f.getType() == DbFieldType.STRING)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Predicate> 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<String, String> params, Map<String, MultipartFile> 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();
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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<QueryFilter> 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<String, String> params, Map<String, MultipartFile> 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(", "));
|
||||
schema.getJpaRepository().update(schema, params, files);
|
||||
|
||||
String query = "UPDATE `" + schema.getTableName() + "` SET " + updateFields + " WHERE `" + schema.getPrimaryKey().getName() + "` = ?";
|
||||
jdbcTemplate.update(query, updateArray);
|
||||
}
|
||||
|
||||
@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<QueryFilter> 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<DbObject> 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();
|
||||
}
|
||||
|
@ -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<CompareOperator> 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<CompareOperator> 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<CompareOperator> 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<CompareOperator> 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<CompareOperator> 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<CompareOperator> 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<CompareOperator> 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<CompareOperator> 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<CompareOperator> 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<CompareOperator> 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<CompareOperator> 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<CompareOperator> 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<CompareOperator> getCompareOperators() {
|
||||
throw new DbAdminException();
|
||||
}
|
||||
},
|
||||
COMPUTED {
|
||||
@Override
|
||||
@ -272,6 +338,11 @@ public enum DbFieldType {
|
||||
public Class<?> getJavaClass() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
throw new DbAdminException();
|
||||
}
|
||||
};
|
||||
|
||||
public abstract String getHTMLName();
|
||||
@ -280,6 +351,8 @@ public enum DbFieldType {
|
||||
|
||||
public abstract Class<?> getJavaClass();
|
||||
|
||||
public abstract List<CompareOperator> getCompareOperators();
|
||||
|
||||
public boolean isRelationship() {
|
||||
return false;
|
||||
}
|
||||
|
@ -17,9 +17,14 @@ public class DbFieldValue {
|
||||
}
|
||||
|
||||
public String getFormattedValue() {
|
||||
if (field.getFormat() == null) return value == null ? "NULL" : value.toString();
|
||||
if (value == null) return null;
|
||||
|
||||
if (field.getFormat() == null) {
|
||||
return value.toString();
|
||||
} else {
|
||||
return String.format(field.getFormat(), value);
|
||||
}
|
||||
}
|
||||
|
||||
public DbField getField() {
|
||||
return field;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
@ -174,97 +173,13 @@ public class DbObjectSchema {
|
||||
return computedColumns.get(name);
|
||||
}
|
||||
|
||||
public Object[] getInsertArray(Map<String, String> params, Map<String, MultipartFile> 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()];
|
||||
public List<DbField> getFilterableFields() {
|
||||
return getSortedFields().stream().filter(f -> {
|
||||
return !f.isBinary() && !f.isPrimaryKey()
|
||||
&& f.getPrimitiveField().getAnnotation(Filterable.class) != null;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
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<String, String> params, Map<String, MultipartFile> 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;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DbObjectSchema [fields=" + fields + ", className=" + entityClass.getName() + "]";
|
||||
|
52
src/main/java/tech/ailef/dbadmin/dto/CompareOperator.java
Normal file
52
src/main/java/tech/ailef/dbadmin/dto/CompareOperator.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
37
src/main/java/tech/ailef/dbadmin/dto/ListModelRequest.java
Normal file
37
src/main/java/tech/ailef/dbadmin/dto/ListModelRequest.java
Normal file
@ -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<QueryFilter> queryFilters;
|
||||
//
|
||||
// private PaginationInfo paginationInfo;
|
||||
//
|
||||
// public ListModelRequest(String className, String query, Integer page, Integer pageSize, String sortKey,
|
||||
// String sortOrder, Set<QueryFilter> 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<String, String> otherParams,
|
||||
//}
|
@ -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.
|
||||
@ -31,11 +37,24 @@ public class PaginationInfo {
|
||||
|
||||
private long maxElement;
|
||||
|
||||
public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement) {
|
||||
private Set<QueryFilter> queryFilters;
|
||||
|
||||
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<QueryFilter> 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() {
|
||||
@ -66,6 +85,36 @@ public class PaginationInfo {
|
||||
return maxElement;
|
||||
}
|
||||
|
||||
public String getSortedPageLink(String sortKey, String sortOrder) {
|
||||
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", "" + currentPage);
|
||||
params.add("sortKey", sortKey);
|
||||
params.add("sortOrder", sortOrder);
|
||||
|
||||
return Utils.getQueryString(params);
|
||||
}
|
||||
|
||||
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() {
|
||||
return IntStream.range(Math.max(currentPage - PAGE_RANGE, 1), currentPage).boxed().collect(Collectors.toList());
|
||||
}
|
||||
|
57
src/main/java/tech/ailef/dbadmin/dto/QueryFilter.java
Normal file
57
src/main/java/tech/ailef/dbadmin/dto/QueryFilter.java
Normal file
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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<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().toString());
|
||||
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, CompareOperator.valueOf(op.toUpperCase()), 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) {
|
||||
boolean shouldConvertNextCharToLower = true;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
**/
|
||||
@ -155,3 +166,11 @@ AUTOCOMPLETE
|
||||
.clear-all-badge {
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters
|
||||
*/
|
||||
|
||||
.filterable-field .card-header:hover {
|
||||
background-color: #F0F0F0;
|
||||
}
|
34
src/main/resources/static/js/filters.js
Normal file
34
src/main/resources/static/js/filters.js
Normal file
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
@ -48,13 +48,24 @@
|
||||
</a>
|
||||
</th:block>
|
||||
<th:block th:if="${!field.isPrimaryKey()}">
|
||||
<span th:text="${object.get(field).getFormattedValue()}" th:if="${!field.isBinary()}">
|
||||
|
||||
<th:block th:if="${!field.isBinary()}">
|
||||
<span th:if="${object.get(field).getFormattedValue() == null}" class="font-monospace null-label">
|
||||
NULL
|
||||
</span>
|
||||
<span th:unless="${object.get(field).getFormattedValue() == null}"
|
||||
th:text="${object.get(field).getFormattedValue()}">
|
||||
|
||||
</span>
|
||||
|
||||
</th:block>
|
||||
<span th:unless="${!field.isBinary()}">
|
||||
<th:block th:if="${object.get(field).getValue()}">
|
||||
<a class="text-decoration-none null-label"
|
||||
th:href="|/dbadmin/download/${schema.getJavaClass().getName()}/${field.getName()}/${object.get(schema.getPrimaryKey()).getValue()}|">
|
||||
<i class="align-middle bi bi-box-arrow-down"></i><span class="align-middle"> Download</span>
|
||||
<i class="align-middle bi bi-box-arrow-down"></i><span class="align-middle"> Download
|
||||
<!--/*--> <span class="text-muted">([[ ${object.get(field).getValue().length} ]] bytes)</span> <!--*/-->
|
||||
</span>
|
||||
</a>
|
||||
</th:block>
|
||||
<th:block th:unless="${object.get(field).getValue()}">
|
||||
|
@ -38,5 +38,73 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="card mb-3 filterable-field" th:fragment="filter_field(field)">
|
||||
<div class="card-header noselect cursor-pointer">
|
||||
<i class="bi bi-caret-right filter-icon align-middle"></i>
|
||||
<span class="fw-bold align-middle" th:text="${field.getName()}"></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<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" th:value="${page.getPagination().getPageSize()}">
|
||||
<input type="hidden" name="query" th:value="${query}">
|
||||
<input type="hidden" name="filter_field" th:value="${field.getJavaName()}">
|
||||
|
||||
<div class="input-group pe-2">
|
||||
<th:block th:if="${field.isForeignKey()}">
|
||||
<span class="input-group-text w-25">
|
||||
<input type="hidden" name="filter_op" value="string_eq">
|
||||
Equals
|
||||
</span>
|
||||
<div class="autocomplete-input position-relative w-50">
|
||||
<input class="autocomplete form-control" type="text" name="filter_value"
|
||||
th:data-classname="${field.getConnectedType().getName()}"
|
||||
autocomplete="off"
|
||||
placeholder="NULL">
|
||||
</input>
|
||||
<div class="suggestions d-none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</th:block>
|
||||
<th:block th:unless="${field.isForeignKey()}">
|
||||
<select class="form-select w-25" name="filter_op">
|
||||
<option th:value="${op}" th:each="op : ${field.getType().getCompareOperators()}"
|
||||
th:text="${op.getDisplayName()}">
|
||||
</select>
|
||||
<input placeholder="NULL" th:type="${field.getType().getHTMLName()}"
|
||||
name="filter_value"
|
||||
class="form-control w-50" th:id="|__id_${field.getName()}|"
|
||||
th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}"
|
||||
th:required="${!field.isNullable() && !field.isPrimaryKey()}"
|
||||
step="any"
|
||||
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
||||
oninput="this.setCustomValidity('')">
|
||||
</th:block>
|
||||
|
||||
<th:block th:each="p : ${queryParams.keySet()}">
|
||||
<input th:each="v : ${queryParams.get(p)}"
|
||||
th:name="${p}" th:value="${v}" type="hidden"
|
||||
th:if="${p.startsWith('filter_')}">
|
||||
</th:block>
|
||||
|
||||
|
||||
<button class="ui-btn btn btn-primary"><i class="bi bi-search text-white"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!--
|
||||
<th:block th:if="${field.getConnectedType() != null}">
|
||||
<div th:each="val : ${schema.getFieldValues(field)}">
|
||||
<span th:text="${val}"></span>
|
||||
</div>
|
||||
</th:block>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -9,6 +9,7 @@
|
||||
<script type="text/javascript" src="/js/table.js"></script>
|
||||
<script type="text/javascript" src="/js/autocomplete.js"></script>
|
||||
<script type="text/javascript" src="/js/autocomplete-multi.js"></script>
|
||||
<script type="text/javascript" src="/js/filters.js"></script>
|
||||
<title th:text="${title != null ? title + ' | Spring Boot DB Admin Panel' : 'Spring Boot DB Admin Panel'}"></title>
|
||||
</head>
|
||||
|
||||
@ -124,14 +125,17 @@
|
||||
<div th:if="${page != null && page.getPagination().getMaxPage() != 1}" class="d-flex">
|
||||
<ul class="pagination me-3">
|
||||
<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">«</span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<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 class="page-item active">
|
||||
@ -139,12 +143,15 @@
|
||||
</li>
|
||||
|
||||
<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 class="page-item">
|
||||
<a class="page-link"
|
||||
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 aria-hidden="true">»</span>
|
||||
</a>
|
||||
@ -155,6 +162,11 @@
|
||||
<input type="hidden" th:value="${page.getPagination().getCurrentPage()}" th:name="page">
|
||||
<input type="hidden" th:value="${query}" th:name="query">
|
||||
<input type="hidden" name="pageSize">
|
||||
<th:block th:each="p : ${queryParams.keySet()}">
|
||||
<input th:each="v : ${queryParams.get(p)}"
|
||||
th:name="${p}" th:value="${v}" type="hidden"
|
||||
th:if="${p.startsWith('filter_')}">
|
||||
</th:block>
|
||||
<select class="form-select page-size">
|
||||
<option disabled>Page size</option>
|
||||
<option th:selected="${page.getPagination().getPageSize() == 50}">50</option>
|
||||
@ -173,6 +185,20 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center" th:if="${page.getPagination().getMaxPage() == 1}">
|
||||
<div class="me-3">
|
||||
<form method="GET" th:action="@{|/dbadmin/model/${schema.getClassName()}|}">
|
||||
<input type="hidden" th:value="${page.getPagination().getCurrentPage()}" th:name="page">
|
||||
<input type="hidden" th:value="${query}" th:name="query">
|
||||
<input type="hidden" name="pageSize">
|
||||
<select class="form-select page-size">
|
||||
<option disabled>Page size</option>
|
||||
<option th:selected="${page.getPagination().getPageSize() == 50}">50</option>
|
||||
<option th:selected="${page.getPagination().getPageSize() == 100}">100</option>
|
||||
<option th:selected="${page.getPagination().getPageSize() == 150}">150</option>
|
||||
<option th:selected="${page.getPagination().getPageSize() == 200}">200</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
<p class="m-0 p-0">
|
||||
<i>Showing [[ ${page.getActualResults()} ]] of [[ ${page.getPagination().getMaxElement()} ]] results</i>
|
||||
</p>
|
||||
@ -180,7 +206,6 @@
|
||||
|
||||
|
||||
<div class="bulk-actions">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -27,21 +27,18 @@
|
||||
</div>
|
||||
<div class="align-items-center">
|
||||
<h4 class="m-0" th:if="${page}">
|
||||
<th:block th:if="${sortKey != field.getName()}" >
|
||||
<a th:href="@{|/dbadmin/model/${schema.getClassName()}|(page=${page.getPagination().getCurrentPage()},
|
||||
pageSize=${page.getPagination().getPageSize()},sortKey=${field.getName()},sortOrder=DESC)}">
|
||||
<th:block th:if="${sortKey != field.getJavaName()}" >
|
||||
<a th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getSortedPageLink(field.getJavaName(), 'DESC')}|}">
|
||||
<i title="Sort" class="bi bi-caret-up"></i>
|
||||
</a>
|
||||
</th:block>
|
||||
<th:block th:unless="${sortKey != field.getName()}">
|
||||
<th:block th:unless="${sortKey != field.getJavaName()}">
|
||||
<a th:if="${sortOrder == 'DESC'}"
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}|(page=${page.getPagination().getCurrentPage()},
|
||||
pageSize=${page.getPagination().getPageSize()},sortKey=${field.getName()},sortOrder=ASC)}">
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getSortedPageLink(field.getJavaName(), 'ASC')}|}">
|
||||
<i title="Sort" class="bi bi-caret-down-fill"></i>
|
||||
</a>
|
||||
<a th:if="${sortOrder == 'ASC'}"
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}|(page=${page.getPagination().getCurrentPage()},
|
||||
pageSize=${page.getPagination().getPageSize()},sortKey=${field.getName()},sortOrder=DESC)}">
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getSortedPageLink(field.getJavaName(), 'DESC')}|}">
|
||||
<i title="Sort" class="bi bi-caret-up-fill"></i>
|
||||
</a>
|
||||
</th:block>
|
||||
|
@ -39,7 +39,7 @@
|
||||
: (object != null ? object.traverse(field).getPrimaryKeyValue() : '' )
|
||||
})}">
|
||||
</div>
|
||||
<input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|">
|
||||
<!-- <input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|"> -->
|
||||
</th:block>
|
||||
<th:block th:unless="${field.isForeignKey()}">
|
||||
<input placeholder="NULL" th:type="${field.getType().getHTMLName()}"
|
||||
@ -55,7 +55,7 @@
|
||||
step="any"
|
||||
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
||||
oninput="this.setCustomValidity('')">
|
||||
<input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|">
|
||||
<!-- <input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|"> -->
|
||||
</th:block>
|
||||
</div>
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
<span class="align-middle"> [[ ${schema.getJavaClass().getSimpleName()} ]] </span>
|
||||
</h1>
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
<div th:class="${schema.getFilterableFields().isEmpty() ? 'col' : 'col-9'}">
|
||||
<div class="w-100 d-flex inner-navigation">
|
||||
<a th:href="|/dbadmin/model/${className}|" class="active">
|
||||
<div class="ui-tab ps-5 pe-5 p-3">
|
||||
@ -33,31 +33,77 @@
|
||||
<div class="box with-navigation">
|
||||
<form th:action="|/dbadmin/model/${className}|" method="GET" class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" th:value="${query}"
|
||||
placeholder="Type and press ENTER to search"
|
||||
class="ui-text-input form-control" name="query" autofocus>
|
||||
<button class="ui-btn btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
<th:block th:each="queryParam : ${queryParams.keySet()}">
|
||||
<input th:each="paramValue : ${queryParams.get(queryParam)}"
|
||||
th:if="${queryParam.startsWith('filter_')}"
|
||||
th:name="${queryParam}" th:value="${paramValue}" type="hidden">
|
||||
</th:block>
|
||||
|
||||
<input type="hidden" name="page" value="1">
|
||||
<input type="hidden" name="pageSize"
|
||||
th:value="${page.getPagination().getPageSize()}">
|
||||
</form>
|
||||
<div class="separator mb-4 mt-4"></div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
|
||||
|
||||
<h3 class="fw-bold mb-4 align-baseline w-100">
|
||||
<h3 class="fw-bold mb-4 align-baseline flex-grow-1">
|
||||
<span title="Java class name"> [[ ${schema.getJavaClass().getSimpleName()} ]] </span>
|
||||
<span title="Database table name" class="ms-3 label label-primary label-gray font-monospace">
|
||||
[[ ${schema.getTableName()} ]]
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</h3>
|
||||
<h3><a title="Create new item"
|
||||
th:href="|/dbadmin/model/${schema.getClassName()}/create|"><i class="bi bi-plus-square"></i></a></h3>
|
||||
|
||||
<h3>
|
||||
<a title="Create new item"
|
||||
th:href="|/dbadmin/model/${schema.getClassName()}/create|"><i class="bi bi-plus-square"></i></a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div th:replace="~{fragments/table_selectable :: table(results=${page.getResults()}, schema=${schema})}"
|
||||
<div th:replace="~{fragments/table_selectable :: table(results=${page.getResults()}, schema=${schema})}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:if="${!schema.getFilterableFields().isEmpty()}" class="col-3">
|
||||
<div class="box filterable-fields">
|
||||
<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()}">
|
||||
<div th:replace="~{fragments/forms :: filter_field(field=${field})}"></div>
|
||||
</th:block>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user