Backend log filters, pagination

This commit is contained in:
Jake Potrebic 2021-05-19 14:41:01 -07:00
parent c606afc144
commit a1c10bf84a
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
13 changed files with 221 additions and 36 deletions

View File

@ -2,7 +2,16 @@
<v-card>
<v-card-title>{{ $t('userActionLog.title') }}</v-card-title>
<v-card-text>
<v-data-table :items="loggedActions.result" :headers="headers">
<v-data-table
:items="loggedActions.result"
:headers="headers"
:server-items-length="loggedActions.pagination.count"
:items-per-page="50"
:options.sync="options"
:footer-props="{ itemsPerPageOptions: [10, 25, 50] }"
:loading="loading"
disable-sort
>
<template #item.user="{ item }">
<NuxtLink :to="'/' + item.userName">{{ item.userName }}</NuxtLink>
</template>
@ -73,22 +82,26 @@
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator';
import { Component } from 'nuxt-property-decorator';
import { LoggedAction } from 'hangar-internal';
import { Context } from '@nuxt/types';
import { PaginatedResult } from 'hangar-api';
import { DataOptions } from 'vuetify';
import { GlobalPermission } from '~/utils/perms';
import { NamedPermission } from '~/types/enums';
import MarkdownModal from '~/components/modals/MarkdownModal.vue';
import DiffModal from '~/components/modals/DiffModal.vue';
import { HangarComponent } from '~/components/mixins';
// TODO figure out a nice way to do filters for AdminLogPage
@Component({
components: { DiffModal, MarkdownModal },
})
@GlobalPermission(NamedPermission.VIEW_LOGS)
export default class AdminLogPage extends Vue {
export default class AdminLogPage extends HangarComponent {
loggedActions!: PaginatedResult<LoggedAction>;
loading = false;
options = { page: 1, itemsPerPage: 50 } as DataOptions;
headers = [
{ text: this.$t('userActionLog.user'), value: 'user' },
{ text: this.$t('userActionLog.address'), value: 'address' },
@ -105,6 +118,31 @@ export default class AdminLogPage extends Vue {
};
}
// TODO I'd like to move these things to a mixin since they are common across multiple components (see authors.vue, staff.vue, etc.)
mounted() {
this.$watch('options', this.onOptionsChanged, { deep: true });
}
onOptionsChanged() {
this.loading = true;
this.$api
.requestInternal<PaginatedResult<LoggedAction>>('admin/log', true, 'get', this.requestOptions)
.then((log) => {
this.loggedActions = log;
})
.catch(this.$util.handleRequestError)
.finally(() => {
this.loading = false;
});
}
get requestOptions() {
return {
limit: this.options.itemsPerPage,
offset: (this.options.page - 1) * this.options.itemsPerPage,
};
}
async asyncData({ $api, $util }: Context) {
const loggedActions = await $api.requestInternal<PaginatedResult<LoggedAction>>(`admin/log/`, false).catch<any>($util.handlePageRequestError);
return { loggedActions };

View File

@ -3,6 +3,7 @@ package io.papermc.hangar.controller.extras;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.util.StaticContextAccessor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
@ -19,8 +20,12 @@ public class ApiUtils {
* @return actual limit
*/
public static long limitOrDefault(@Nullable Long limit) {
return limitOrDefault(limit, hangarConfig.projects.getInitLoad());
}
public static long limitOrDefault(@Nullable Long limit, long maxLimit) {
if (limit != null && limit < 1) throw new HangarApiException(HttpStatus.BAD_REQUEST, "Limit should be greater than 0");
return Math.min(limit == null ? hangarConfig.projects.getInitLoad() : limit, hangarConfig.projects.getInitLoad());
return Math.min(limit == null ? maxLimit : limit, maxLimit);
}
/**

View File

@ -19,7 +19,7 @@ public interface Filter<F extends FilterInstance> {
String getDescription();
default boolean supports(NativeWebRequest webRequest) {
return webRequest.getParameterMap().containsKey(getQueryParamNames());
return webRequest.getParameterMap().containsKey(getSingleQueryParam());
}
@NotNull

View File

@ -0,0 +1,21 @@
package io.papermc.hangar.controller.extras.pagination.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Configure default page length/number
*/
@Documented
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigurePagination {
/**
* -1 means fallback to default configured value
*/
long maxLimit();
}

View File

@ -0,0 +1,51 @@
package io.papermc.hangar.controller.extras.pagination.filters.log;
import io.papermc.hangar.controller.extras.pagination.Filter;
import io.papermc.hangar.controller.extras.pagination.filters.log.LogActionFilter.LogActionFilterInstance;
import io.papermc.hangar.model.internal.logs.LogAction;
import org.jdbi.v3.core.statement.SqlStatement;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.NativeWebRequest;
import java.util.Set;
@Component
public class LogActionFilter implements Filter<LogActionFilterInstance> {
@Override
public Set<String> getQueryParamNames() {
return Set.of("logAction");
}
@Override
public String getDescription() {
return "Filters by log action";
}
@Override
public boolean supports(NativeWebRequest webRequest) {
return Filter.super.supports(webRequest) && LogAction.LOG_REGISTRY.containsKey(webRequest.getParameter(getSingleQueryParam()));
}
@NotNull
@Override
public LogActionFilterInstance create(NativeWebRequest webRequest) {
return new LogActionFilterInstance(LogAction.LOG_REGISTRY.get(webRequest.getParameter(getSingleQueryParam())));
}
static class LogActionFilterInstance implements FilterInstance {
private final LogAction<?> logAction;
LogActionFilterInstance(LogAction<?> logAction) {
this.logAction = logAction;
}
@Override
public void createSql(StringBuilder sb, SqlStatement<?> q) {
sb.append(" AND la.action = :actionFilter::LOGGED_ACTION_TYPE");
q.bind("actionFilter", logAction.getPgLoggedAction().getValue());
}
}
}

View File

@ -0,0 +1,45 @@
package io.papermc.hangar.controller.extras.pagination.filters.log;
import io.papermc.hangar.controller.extras.pagination.Filter;
import io.papermc.hangar.controller.extras.pagination.filters.log.LogSubjectFilter.LogSubjectFilterInstance;
import org.jdbi.v3.core.statement.SqlStatement;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.NativeWebRequest;
import java.util.Set;
@Component
public class LogSubjectFilter implements Filter<LogSubjectFilterInstance> {
@Override
public Set<String> getQueryParamNames() {
return Set.of("subjectName");
}
@Override
public String getDescription() {
return "Filters by subject name, usually a user action where the subject name is the user the action is about, not the user that performed the action";
}
@NotNull
@Override
public LogSubjectFilterInstance create(NativeWebRequest webRequest) {
return new LogSubjectFilterInstance(webRequest.getParameter(getSingleQueryParam()));
}
static class LogSubjectFilterInstance implements FilterInstance {
private final String subjectName;
LogSubjectFilterInstance(String subjectName) {
this.subjectName = subjectName;
}
@Override
public void createSql(StringBuilder sb, SqlStatement<?> q) {
sb.append(" AND la.s_name = :subjectName");
q.bind("subjectName", this.subjectName);
}
}
}

View File

@ -7,6 +7,7 @@ import io.papermc.hangar.controller.extras.pagination.FilterRegistry;
import io.papermc.hangar.controller.extras.pagination.SorterRegistry;
import io.papermc.hangar.controller.extras.pagination.annotations.ApplicableFilters;
import io.papermc.hangar.controller.extras.pagination.annotations.ApplicableSorters;
import io.papermc.hangar.controller.extras.pagination.annotations.ConfigurePagination;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.api.requests.RequestPagination;
import org.jetbrains.annotations.NotNull;
@ -45,8 +46,15 @@ public class RequestPaginationResolver implements HandlerMethodArgumentResolver
@Override
public RequestPagination resolveArgument(@NotNull MethodParameter parameter, ModelAndViewContainer mavContainer, @NotNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
long limit = ApiUtils.limitOrDefault(Optional.ofNullable(webRequest.getParameter("limit")).map(Long::parseLong).orElse(null));
long offset = ApiUtils.offsetOrZero(Optional.ofNullable(webRequest.getParameter("offset")).map(Long::parseLong).orElse(null));
long limit;
Optional<Long> limitParam = Optional.ofNullable(webRequest.getParameter("limit")).map(Long::parseLong);
ConfigurePagination settings = parameter.getParameterAnnotation(ConfigurePagination.class);
if (settings != null) {
limit = ApiUtils.limitOrDefault(limitParam.orElse(null), settings.maxLimit());
} else {
limit = ApiUtils.limitOrDefault(limitParam.orElse(null));
}
RequestPagination pagination = new RequestPagination(limit, offset);
// find filters

View File

@ -3,9 +3,18 @@ package io.papermc.hangar.controller.internal.admin;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.controller.extras.pagination.annotations.ApplicableFilters;
import io.papermc.hangar.controller.extras.pagination.annotations.ConfigurePagination;
import io.papermc.hangar.controller.extras.pagination.filters.log.LogActionFilter;
import io.papermc.hangar.controller.extras.pagination.filters.log.LogPageFilter;
import io.papermc.hangar.controller.extras.pagination.filters.log.LogProjectFilter;
import io.papermc.hangar.controller.extras.pagination.filters.log.LogSubjectFilter;
import io.papermc.hangar.controller.extras.pagination.filters.log.LogUserFilter;
import io.papermc.hangar.controller.extras.pagination.filters.log.LogVersionFilter;
import io.papermc.hangar.controller.extras.resolvers.NoCache;
import io.papermc.hangar.model.api.PaginatedResult;
import io.papermc.hangar.model.api.Pagination;
import io.papermc.hangar.model.api.requests.RequestPagination;
import io.papermc.hangar.model.common.NamedPermission;
import io.papermc.hangar.model.db.JobTable;
import io.papermc.hangar.model.db.UserTable;
@ -21,6 +30,7 @@ import io.papermc.hangar.service.internal.PlatformService;
import io.papermc.hangar.service.internal.admin.HealthService;
import io.papermc.hangar.service.internal.admin.StatService;
import io.papermc.hangar.service.internal.users.UserService;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
@ -103,12 +113,12 @@ public class AdminController extends HangarComponent {
userService.setLocked(user, locked, comment.getContent());
}
// TODO filters/pagination
@ResponseBody
@GetMapping(value = "/log", produces = MediaType.APPLICATION_JSON_VALUE)
@PermissionRequired(NamedPermission.REVIEWER)
public PaginatedResult<HangarLoggedAction> getActionLog() {
List<HangarLoggedAction> log = actionLogger.getLog(0, null, null, null, null, null, null);
return new PaginatedResult<>(new Pagination(25, 0, (long) log.size()), log);
@ApplicableFilters({LogActionFilter.class, LogPageFilter.class, LogProjectFilter.class, LogSubjectFilter.class, LogUserFilter.class, LogVersionFilter.class})
// TODO add sorters
public PaginatedResult<HangarLoggedAction> getActionLog(@NotNull @ConfigurePagination(maxLimit = 50) RequestPagination pagination) {
return actionLogger.getLogs(pagination);
}
}

View File

@ -1,6 +1,8 @@
package io.papermc.hangar.db.dao.internal;
import io.papermc.hangar.db.extras.BindPagination;
import io.papermc.hangar.db.mappers.LogActionColumnMapper;
import io.papermc.hangar.model.api.requests.RequestPagination;
import io.papermc.hangar.model.db.log.LoggedActionsOrganizationTable;
import io.papermc.hangar.model.db.log.LoggedActionsPageTable;
import io.papermc.hangar.model.db.log.LoggedActionsProjectTable;
@ -46,14 +48,14 @@ public interface LoggedActionsDAO {
@RegisterColumnMapper(LogActionColumnMapper.class)
@RegisterConstructorMapper(HangarLoggedAction.class)
@SqlQuery("SELECT * FROM v_logged_actions la " +
" WHERE true " +
"<if(userFilter)>AND la.user_name = :userFilter<endif> " +
"<if(projectFilter)>AND (la.p_owner_name || '/' || la.p_slug) = :projectFilter<endif> " +
"<if(versionFilter)>AND la.pv_version_string = :versionFilter<endif> " +
"<if(pageFilter)>AND la.pp_id = :pageFilter<endif> " +
"<if(actionFilter)>AND la.action = :actionFilter::LOGGED_ACTION_TYPE<endif> " +
"<if(subjectFilter)>AND la.s_name = :subjectFilter<endif> " +
"ORDER BY la.created_at DESC OFFSET :offset LIMIT :pageSize")
" WHERE true <filters>" +
" ORDER BY la.created_at DESC <offsetLimit>")
// TODO add <sorters>
@DefineNamedBindings
List<HangarLoggedAction> getLog(String userFilter, String projectFilter, String versionFilter, String pageFilter, String actionFilter, String subjectFilter, long offset, long pageSize);
List<HangarLoggedAction> getLog(@BindPagination RequestPagination pagination);
@UseStringTemplateEngine
@SqlQuery("SELECT count(*) FROM v_logged_actions la " +
" WHERE true <filters>")
long getLogCount(@BindPagination(isCount = true) RequestPagination pagination);
}

View File

@ -7,6 +7,7 @@ import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizingAnnotation;
import org.jdbi.v3.sqlobject.customizer.SqlStatementParameterCustomizer;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -15,11 +16,20 @@ import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
/**
* Configure filters, sorters, offset, and limit from a web request
* into a db query
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
@SqlStatementCustomizingAnnotation(BindPagination.BindPaginationFactory.class)
public @interface BindPagination {
/**
* set to true to disable the injection of sorters, offset, and limit.<br>
* used for getting the total count of all entries
*/
boolean isCount() default false;
class BindPaginationFactory implements SqlStatementCustomizerFactory {

View File

@ -6,13 +6,9 @@ public class Pagination extends RequestPagination {
private final long count;
public Pagination(long limit, long offset, Long count) {
super(limit, offset);
this.count = count != null ? count : 0;
}
public Pagination(Long count, RequestPagination pagination) {
this(pagination.getLimit(), pagination.getOffset(), count);
super(pagination.getLimit(), pagination.getOffset());
this.count = count != null ? count : 0;
}
public long getCount() {

View File

@ -27,9 +27,12 @@ public class RequestPagination {
@ApiModelProperty(hidden = true)
private final Map<String, Consumer<StringBuilder>> sorters;
/**
* limit/offset params should be validated before construction
*/
public RequestPagination(Long limit, Long offset) {
this.limit = ApiUtils.limitOrDefault(limit);
this.offset = ApiUtils.offsetOrZero(offset);
this.limit = limit;
this.offset = offset;
this.filters = new ArrayList<>();
this.sorters = new LinkedHashMap<>();
}

View File

@ -3,6 +3,9 @@ package io.papermc.hangar.service.internal;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.internal.LoggedActionsDAO;
import io.papermc.hangar.model.api.PaginatedResult;
import io.papermc.hangar.model.api.Pagination;
import io.papermc.hangar.model.api.requests.RequestPagination;
import io.papermc.hangar.model.db.log.LoggedActionTable;
import io.papermc.hangar.model.internal.logs.HangarLoggedAction;
import io.papermc.hangar.model.internal.logs.LoggedAction;
@ -55,14 +58,7 @@ public class UserActionLogService extends HangarComponent {
inserter.accept(action.getContext().createTable(getHangarPrincipal().getUserId(), RequestUtil.getRemoteInetAddress(request), action));
}
public List<HangarLoggedAction> getLog(Integer oPage, String userFilter, String projectFilter, String versionFilter, String pageFilter, String actionFilter, String subjectFilter) {
long pageSize = 50L;
long offset;
if (oPage == null) {
offset = 0;
} else {
offset = oPage * pageSize;
}
return loggedActionsDAO.getLog(userFilter, projectFilter, versionFilter, pageFilter, actionFilter, subjectFilter, offset, pageSize);
public PaginatedResult<HangarLoggedAction> getLogs(RequestPagination pagination) {
return new PaginatedResult<>(new Pagination(loggedActionsDAO.getLogCount(pagination), pagination), loggedActionsDAO.getLog(pagination));
}
}