Implemented sorting on Action logs and standardized filtering/pagination mechanics

This commit is contained in:
Francesco 2023-09-30 11:22:56 +02:00
parent 6e2cb29f82
commit fa51f11109
12 changed files with 190 additions and 63 deletions

View File

@ -33,6 +33,7 @@ import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.external.dbmapping.DbObject; import tech.ailef.dbadmin.external.dbmapping.DbObject;
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema; import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.external.dto.CompareOperator; import tech.ailef.dbadmin.external.dto.CompareOperator;
import tech.ailef.dbadmin.external.dto.FacetedSearchRequest;
import tech.ailef.dbadmin.external.dto.LogsSearchRequest; import tech.ailef.dbadmin.external.dto.LogsSearchRequest;
import tech.ailef.dbadmin.external.dto.PaginatedResult; import tech.ailef.dbadmin.external.dto.PaginatedResult;
import tech.ailef.dbadmin.external.dto.QueryFilter; import tech.ailef.dbadmin.external.dto.QueryFilter;
@ -140,7 +141,8 @@ public class DefaultDbAdminController {
queryFilters.removeIf(f -> f.equals(toRemove)); queryFilters.removeIf(f -> f.equals(toRemove));
} }
MultiValueMap<String, String> parameterMap = Utils.computeParams(queryFilters); FacetedSearchRequest filterRequest = new FacetedSearchRequest(queryFilters);
MultiValueMap<String, String> parameterMap = filterRequest.computeParams();
MultiValueMap<String, String> filteredParams = new LinkedMultiValueMap<>(); MultiValueMap<String, String> filteredParams = new LinkedMultiValueMap<>();
request.getParameterMap().entrySet().stream() request.getParameterMap().entrySet().stream()
@ -437,12 +439,7 @@ public class DefaultDbAdminController {
model.addAttribute("activePage", "logs"); model.addAttribute("activePage", "logs");
model.addAttribute( model.addAttribute(
"page", "page",
userActionService.findActions( userActionService.findActions(searchRequest)
searchRequest.getTable(),
searchRequest.getActionType(),
searchRequest.getItemId(),
searchRequest.toPageRequest()
)
); );
model.addAttribute("schemas", dbAdmin.getSchemas()); model.addAttribute("schemas", dbAdmin.getSchemas());
model.addAttribute("searchRequest", searchRequest); model.addAttribute("searchRequest", searchRequest);

View File

@ -3,7 +3,6 @@ package tech.ailef.dbadmin.external.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;
@ -21,6 +20,7 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import tech.ailef.dbadmin.external.dto.FacetedSearchRequest;
import tech.ailef.dbadmin.external.dto.PaginatedResult; import tech.ailef.dbadmin.external.dto.PaginatedResult;
import tech.ailef.dbadmin.external.dto.PaginationInfo; import tech.ailef.dbadmin.external.dto.PaginationInfo;
import tech.ailef.dbadmin.external.dto.QueryFilter; import tech.ailef.dbadmin.external.dto.QueryFilter;
@ -118,7 +118,7 @@ public class DbAdminRepository {
return new PaginatedResult<DbObject>( return new PaginatedResult<DbObject>(
new PaginationInfo(page, maxPage, pageSize, maxElement, null, new HashSet<>()), new PaginationInfo(page, maxPage, pageSize, maxElement, null, null),
results results
); );
} }
@ -231,7 +231,7 @@ public class DbAdminRepository {
} }
return new PaginatedResult<DbObject>( return new PaginatedResult<DbObject>(
new PaginationInfo(page, maxPage, pageSize, maxElement, query, queryFilters), new PaginationInfo(page, maxPage, pageSize, maxElement, query, new FacetedSearchRequest(queryFilters)),
jpaRepository.search(query, page, pageSize, sortKey, sortOrder, queryFilters).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

@ -0,0 +1,35 @@
package tech.ailef.dbadmin.external.dto;
import java.util.ArrayList;
import java.util.Set;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
public class FacetedSearchRequest implements FilterRequest {
private Set<QueryFilter> filters;
public FacetedSearchRequest(Set<QueryFilter> filters) {
this.filters = filters;
}
@Override
public MultiValueMap<String, String> computeParams() {
MultiValueMap<String, String> r = new LinkedMultiValueMap<>();
if (filters == null)
return r;
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().getJavaName());
r.get("filter_op").add(filter.getOp().toString());
r.get("filter_value").add(filter.getValue());
}
return r;
}
}

View File

@ -0,0 +1,12 @@
package tech.ailef.dbadmin.external.dto;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
public interface FilterRequest {
public MultiValueMap<String, String> computeParams();
public static MultiValueMap<String, String> empty() {
return new LinkedMultiValueMap<>();
}
}

View File

@ -2,13 +2,15 @@ package tech.ailef.dbadmin.external.dto;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/** /**
* A client request for the Action logs page where * A client request for the Action logs page where
* several filtering parameters are present * several filtering parameters are present
* *
*/ */
public class LogsSearchRequest { public class LogsSearchRequest implements FilterRequest {
/** /**
* The table name to filter on * The table name to filter on
*/ */
@ -125,4 +127,17 @@ public class LogsSearchRequest {
} }
} }
@Override
public MultiValueMap<String, String> computeParams() {
LinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>();
if (table != null)
params.add("table", table);
if (itemId != null)
params.add("itemId", itemId);
if (actionType != null)
params.add("actionType", actionType);
return params;
}
} }

View File

@ -2,7 +2,6 @@ package tech.ailef.dbadmin.external.dto;
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 java.util.stream.IntStream; import java.util.stream.IntStream;
@ -38,17 +37,17 @@ public class PaginationInfo {
// TODO: Check if used // TODO: Check if used
private long maxElement; private long maxElement;
private Set<QueryFilter> queryFilters; private FilterRequest filterRequest;
private String query; private String query;
public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement, String query, Set<QueryFilter> queryFilters) { public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement, String query, FilterRequest request) {
this.currentPage = currentPage; this.currentPage = currentPage;
this.maxPage = maxPage; this.maxPage = maxPage;
this.pageSize = pageSize; this.pageSize = pageSize;
this.query = query; this.query = query;
this.maxElement = maxElement; this.maxElement = maxElement;
this.queryFilters = queryFilters; this.filterRequest = request;
} }
public int getCurrentPage() { public int getCurrentPage() {
@ -80,7 +79,10 @@ public class PaginationInfo {
} }
public String getSortedPageLink(String sortKey, String sortOrder) { public String getSortedPageLink(String sortKey, String sortOrder) {
MultiValueMap<String, String> params = Utils.computeParams(queryFilters); MultiValueMap<String, String> params = FilterRequest.empty();
if (filterRequest != null)
params = filterRequest.computeParams();
if (query != null) { if (query != null) {
params.put("query", new ArrayList<>()); params.put("query", new ArrayList<>());
@ -96,7 +98,10 @@ public class PaginationInfo {
} }
public String getLink(int page) { public String getLink(int page) {
MultiValueMap<String, String> params = Utils.computeParams(queryFilters); MultiValueMap<String, String> params = FilterRequest.empty();
if (filterRequest != null)
params = filterRequest.computeParams();
if (query != null) { if (query != null) {
params.put("query", new ArrayList<>()); params.put("query", new ArrayList<>());

View File

@ -1,12 +1,10 @@
package tech.ailef.dbadmin.external.misc; package tech.ailef.dbadmin.external.misc;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema; import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
@ -61,23 +59,23 @@ public interface Utils {
* @param filters * @param filters
* @return * @return
*/ */
public static MultiValueMap<String, String> computeParams(Set<QueryFilter> filters) { // public static MultiValueMap<String, String> computeParams(Set<QueryFilter> filters) {
MultiValueMap<String, String> r = new LinkedMultiValueMap<>(); // MultiValueMap<String, String> r = new LinkedMultiValueMap<>();
if (filters == null) // if (filters == null)
return r; // return r;
//
r.put("filter_field", new ArrayList<>()); // r.put("filter_field", new ArrayList<>());
r.put("filter_op", new ArrayList<>()); // r.put("filter_op", new ArrayList<>());
r.put("filter_value", new ArrayList<>()); // r.put("filter_value", new ArrayList<>());
//
for (QueryFilter filter : filters) { // for (QueryFilter filter : filters) {
r.get("filter_field").add(filter.getField().getJavaName()); // r.get("filter_field").add(filter.getField().getJavaName());
r.get("filter_op").add(filter.getOp().toString()); // r.get("filter_op").add(filter.getOp().toString());
r.get("filter_value").add(filter.getValue()); // r.get("filter_value").add(filter.getValue());
} // }
//
return r; // return r;
} // }
/** /**
* Converts a multi value map of parameters containing query filters applied * Converts a multi value map of parameters containing query filters applied

View File

@ -2,12 +2,11 @@ package tech.ailef.dbadmin.internal.repository;
import java.util.List; import java.util.List;
import org.springframework.data.domain.PageRequest; import tech.ailef.dbadmin.external.dto.LogsSearchRequest;
import tech.ailef.dbadmin.internal.model.UserAction; import tech.ailef.dbadmin.internal.model.UserAction;
public interface CustomActionRepository { public interface CustomActionRepository {
public List<UserAction> findActions(String table, String actionType, String itemId, PageRequest pageRequest); public List<UserAction> findActions(LogsSearchRequest r);
public long countActions(String table, String actionType, String itemId); public long countActions(String table, String actionType, String itemId);

View File

@ -12,6 +12,7 @@ import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Root;
import tech.ailef.dbadmin.external.dto.LogsSearchRequest;
import tech.ailef.dbadmin.internal.model.UserAction; import tech.ailef.dbadmin.internal.model.UserAction;
@Component @Component
@ -21,7 +22,11 @@ public class CustomActionRepositoryImpl implements CustomActionRepository {
private EntityManager entityManager; private EntityManager entityManager;
@Override @Override
public List<UserAction> findActions(String table, String actionType, String itemId, PageRequest page) { public List<UserAction> findActions(LogsSearchRequest request) {
String table = request.getTable();
String actionType = request.getActionType();
String itemId = request.getItemId();
PageRequest page = request.toPageRequest();
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<UserAction> query = cb.createQuery(UserAction.class); CriteriaQuery<UserAction> query = cb.createQuery(UserAction.class);
@ -41,6 +46,15 @@ public class CustomActionRepositoryImpl implements CustomActionRepository {
predicates.toArray(new Predicate[predicates.size()]))); predicates.toArray(new Predicate[predicates.size()])));
} }
if (request.getSortKey() != null) {
String key = request.getSortKey();
if (request.getSortOrder().equalsIgnoreCase("ASC")) {
query.orderBy(cb.asc(userAction.get(key)));
} else {
query.orderBy(cb.desc(userAction.get(key)));
}
}
return entityManager.createQuery(query) return entityManager.createQuery(query)
.setMaxResults(page.getPageSize()) .setMaxResults(page.getPageSize())
.setFirstResult((int)page.getOffset()) .setFirstResult((int)page.getOffset())

View File

@ -7,6 +7,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import tech.ailef.dbadmin.external.dto.LogsSearchRequest;
import tech.ailef.dbadmin.external.dto.PaginatedResult; import tech.ailef.dbadmin.external.dto.PaginatedResult;
import tech.ailef.dbadmin.external.dto.PaginationInfo; import tech.ailef.dbadmin.external.dto.PaginationInfo;
import tech.ailef.dbadmin.internal.model.UserAction; import tech.ailef.dbadmin.internal.model.UserAction;
@ -26,13 +27,18 @@ public class UserActionService {
return repo.save(a); return repo.save(a);
} }
public PaginatedResult<UserAction> findActions(String table, String actionType, String userId, PageRequest page) { public PaginatedResult<UserAction> findActions(LogsSearchRequest request) {
long count = customRepo.countActions(table, actionType, userId); String table = request.getTable();
List<UserAction> actions = customRepo.findActions(table, actionType, userId, page); String actionType = request.getActionType();
String itemId = request.getItemId();
PageRequest page = request.toPageRequest();
long count = customRepo.countActions(table, actionType, itemId);
List<UserAction> actions = customRepo.findActions(request);
int maxPage = (int)(Math.ceil ((double)count / page.getPageSize())); int maxPage = (int)(Math.ceil ((double)count / page.getPageSize()));
return new PaginatedResult<>( return new PaginatedResult<>(
new PaginationInfo(page.getPageNumber() + 1, maxPage, page.getPageSize(), count, null, null), new PaginationInfo(page.getPageNumber() + 1, maxPage, page.getPageSize(), count, null, request),
actions actions
); );
} }

View File

@ -2,6 +2,23 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head></head> <head></head>
<body> <body>
<th:block th:fragment="sort_arrow(sortKey, sortOrder, fieldName)">
<th:block th:if="${sortKey != fieldName}" >
<a th:href="@{|${requestUrl}${page.getPagination().getSortedPageLink(fieldName, 'DESC')}|}">
<i title="Sort" class="bi bi-caret-up"></i>
</a>
</th:block>
<th:block th:unless="${sortKey != fieldName}">
<a th:if="${sortOrder == 'DESC'}"
th:href="@{|${requestUrl}${page.getPagination().getSortedPageLink(fieldName, 'ASC')}|}">
<i title="Sort" class="bi bi-caret-down-fill"></i>
</a>
<a th:if="${sortOrder == 'ASC'}"
th:href="@{|${requestUrl}${page.getPagination().getSortedPageLink(fieldName, 'DESC')}|}">
<i title="Sort" class="bi bi-caret-up-fill"></i>
</a>
</th:block>
</th:block>
<div class="table-selectable table-responsive" th:fragment="table(results, schema)"> <div class="table-selectable table-responsive" th:fragment="table(results, schema)">
<div th:if="${results.isEmpty()}"> <div th:if="${results.isEmpty()}">
<p>This table contains no data.</p> <p>This table contains no data.</p>
@ -28,21 +45,10 @@
</div> </div>
<div class="align-items-center"> <div class="align-items-center">
<h4 class="m-0" th:if="${page}"> <h4 class="m-0" th:if="${page}">
<th:block th:if="${sortKey != field.getJavaName()}" > <th:block th:replace="~{fragments/table_selectable ::
<a th:href="@{|/${baseUrl}/model/${schema.getClassName()}${page.getPagination().getSortedPageLink(field.getJavaName(), 'DESC')}|}"> sort_arrow(sortKey=${sortKey},
<i title="Sort" class="bi bi-caret-up"></i> sortOrder=${sortOrder},
</a> fieldName=${field.getJavaName()})}">
</th:block>
<th:block th:unless="${sortKey != field.getJavaName()}">
<a th:if="${sortOrder == 'DESC'}"
th:href="@{|/${baseUrl}/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="@{|/${baseUrl}/model/${schema.getClassName()}${page.getPagination().getSortedPageLink(field.getJavaName(), 'DESC')}|}">
<i title="Sort" class="bi bi-caret-up-fill"></i>
</a>
</th:block>
</h4> </h4>
</div> </div>
</div> </div>

View File

@ -59,10 +59,50 @@
<table class="table table-striped mt-3"> <table class="table table-striped mt-3">
<tr class="table-data-row"> <tr class="table-data-row">
<th>Action type</th> <th >
<th>Table</th> <div class="d-flex justify-content-between">
<th>Item ID</th> <div>Action type</div>
<th>Time</th> <h4>
<th:block th:replace="~{fragments/table_selectable ::
sort_arrow(sortKey=${searchRequest.getSortKey()},
sortOrder=${searchRequest.getSortOrder()},
fieldName='actionType')}">
</h4>
</div>
</th>
<th >
<div class="d-flex justify-content-between">
<div>Table</div>
<h4>
<th:block th:replace="~{fragments/table_selectable ::
sort_arrow(sortKey=${searchRequest.getSortKey()},
sortOrder=${searchRequest.getSortOrder()},
fieldName='onTable')}">
</h4>
</div>
</th>
<th >
<div class="d-flex justify-content-between">
<div>Item ID</div>
<h4>
<th:block th:replace="~{fragments/table_selectable ::
sort_arrow(sortKey=${searchRequest.getSortKey()},
sortOrder=${searchRequest.getSortOrder()},
fieldName='primaryKey')}">
</h4>
</div>
</th>
<th >
<div class="d-flex justify-content-between">
<div>Time</div>
<h4>
<th:block th:replace="~{fragments/table_selectable ::
sort_arrow(sortKey=${searchRequest.getSortKey()},
sortOrder=${searchRequest.getSortOrder()},
fieldName='createdAt')}">
</h4>
</div>
</th>
</tr> </tr>
<tr th:each="entry : ${page.getResults()}" class="table-data-row align-middle"> <tr th:each="entry : ${page.getResults()}" class="table-data-row align-middle">
<td th:text="${entry.getActionType()}"> <td th:text="${entry.getActionType()}">