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.DbObjectSchema;
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.PaginatedResult;
import tech.ailef.dbadmin.external.dto.QueryFilter;
@ -140,7 +141,8 @@ public class DefaultDbAdminController {
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<>();
request.getParameterMap().entrySet().stream()
@ -437,12 +439,7 @@ public class DefaultDbAdminController {
model.addAttribute("activePage", "logs");
model.addAttribute(
"page",
userActionService.findActions(
searchRequest.getTable(),
searchRequest.getActionType(),
searchRequest.getItemId(),
searchRequest.toPageRequest()
)
userActionService.findActions(searchRequest)
);
model.addAttribute("schemas", dbAdmin.getSchemas());
model.addAttribute("searchRequest", searchRequest);

View File

@ -3,7 +3,6 @@ package tech.ailef.dbadmin.external.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;
@ -21,6 +20,7 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
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.PaginationInfo;
import tech.ailef.dbadmin.external.dto.QueryFilter;
@ -118,7 +118,7 @@ public class DbAdminRepository {
return new PaginatedResult<DbObject>(
new PaginationInfo(page, maxPage, pageSize, maxElement, null, new HashSet<>()),
new PaginationInfo(page, maxPage, pageSize, maxElement, null, null),
results
);
}
@ -231,7 +231,7 @@ public class DbAdminRepository {
}
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()
.map(o -> new DbObject(o, schema))
.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.Sort;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* A client request for the Action logs page where
* several filtering parameters are present
*
*/
public class LogsSearchRequest {
public class LogsSearchRequest implements FilterRequest {
/**
* The table name to filter on
*/
@ -124,5 +126,18 @@ public class LogsSearchRequest {
return PageRequest.of(actualPage, actualPageSize, Sort.by(sortKey).ascending());
}
}
@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.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@ -38,17 +37,17 @@ public class PaginationInfo {
// TODO: Check if used
private long maxElement;
private Set<QueryFilter> queryFilters;
private FilterRequest filterRequest;
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.maxPage = maxPage;
this.pageSize = pageSize;
this.query = query;
this.maxElement = maxElement;
this.queryFilters = queryFilters;
this.filterRequest = request;
}
public int getCurrentPage() {
@ -80,7 +79,10 @@ public class PaginationInfo {
}
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) {
params.put("query", new ArrayList<>());
@ -96,7 +98,10 @@ public class PaginationInfo {
}
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) {
params.put("query", new ArrayList<>());

View File

@ -1,12 +1,10 @@
package tech.ailef.dbadmin.external.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.external.dbmapping.DbObjectSchema;
@ -61,23 +59,23 @@ public interface Utils {
* @param filters
* @return
*/
public static MultiValueMap<String, String> computeParams(Set<QueryFilter> filters) {
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;
}
// public static MultiValueMap<String, String> computeParams(Set<QueryFilter> filters) {
// 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;
// }
/**
* 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 org.springframework.data.domain.PageRequest;
import tech.ailef.dbadmin.external.dto.LogsSearchRequest;
import tech.ailef.dbadmin.internal.model.UserAction;
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);

View File

@ -12,6 +12,7 @@ import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import tech.ailef.dbadmin.external.dto.LogsSearchRequest;
import tech.ailef.dbadmin.internal.model.UserAction;
@Component
@ -21,7 +22,11 @@ public class CustomActionRepositoryImpl implements CustomActionRepository {
private EntityManager entityManager;
@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();
CriteriaQuery<UserAction> query = cb.createQuery(UserAction.class);
@ -41,6 +46,15 @@ public class CustomActionRepositoryImpl implements CustomActionRepository {
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)
.setMaxResults(page.getPageSize())
.setFirstResult((int)page.getOffset())

View File

@ -7,6 +7,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
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.PaginationInfo;
import tech.ailef.dbadmin.internal.model.UserAction;
@ -26,13 +27,18 @@ public class UserActionService {
return repo.save(a);
}
public PaginatedResult<UserAction> findActions(String table, String actionType, String userId, PageRequest page) {
long count = customRepo.countActions(table, actionType, userId);
List<UserAction> actions = customRepo.findActions(table, actionType, userId, page);
public PaginatedResult<UserAction> findActions(LogsSearchRequest request) {
String table = request.getTable();
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()));
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
);
}

View File

@ -2,6 +2,23 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head></head>
<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 th:if="${results.isEmpty()}">
<p>This table contains no data.</p>
@ -28,21 +45,10 @@
</div>
<div class="align-items-center">
<h4 class="m-0" th:if="${page}">
<th:block th:if="${sortKey != field.getJavaName()}" >
<a th:href="@{|/${baseUrl}/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.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>
<th:block th:replace="~{fragments/table_selectable ::
sort_arrow(sortKey=${sortKey},
sortOrder=${sortOrder},
fieldName=${field.getJavaName()})}">
</h4>
</div>
</div>

View File

@ -59,10 +59,50 @@
<table class="table table-striped mt-3">
<tr class="table-data-row">
<th>Action type</th>
<th>Table</th>
<th>Item ID</th>
<th>Time</th>
<th >
<div class="d-flex justify-content-between">
<div>Action type</div>
<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 th:each="entry : ${page.getResults()}" class="table-data-row align-middle">
<td th:text="${entry.getActionType()}">