Port Admin/stats (#106)

Co-authored-by: KennyTV <jahnke.nassim@gmail.com>
This commit is contained in:
Dragonium 2020-09-01 15:21:06 +02:00 committed by GitHub
parent 8e97d55516
commit 3575848ab5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 278 additions and 86 deletions

View File

@ -7,6 +7,7 @@ $(function() {
function removeTime(date) {
date.setHours(0, 0, 0, 0)
date.setDate(date.getDate() +1);
}
if(!to) {
@ -33,7 +34,7 @@ $(function() {
if(from > to) {
from = new Date();
from.setDate(to.getDate() - 1)
from.setDate(to.getDate() - 2)
}
var url = '/admin/stats?from=' + from.toISOString().substr(0, 10) + '&to=' + to.toISOString().substr(0, 10);

View File

@ -1,7 +1,9 @@
package io.papermc.hangar.controller;
import io.papermc.hangar.db.model.Stats;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.annotation.Secured;
@ -16,6 +18,8 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.ModelAndView;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@ -38,6 +42,7 @@ import io.papermc.hangar.service.SitemapService;
import io.papermc.hangar.service.UserActionLogService;
import io.papermc.hangar.service.UserService;
import io.papermc.hangar.service.VersionService;
import io.papermc.hangar.service.StatsService;
import io.papermc.hangar.service.project.FlagService;
import io.papermc.hangar.service.project.ProjectService;
import io.papermc.hangar.util.AlertUtil;
@ -52,11 +57,12 @@ public class ApplicationController extends HangarController {
private final VersionService versionService;
private final JobService jobService;
private final SitemapService sitemapService;
private final StatsService statsService;
private final HttpServletRequest request;
@Autowired
public ApplicationController(UserService userService, ProjectService projectService, VersionService versionService, FlagService flagService, UserActionLogService userActionLogService, JobService jobService, SitemapService sitemapService, HttpServletRequest request) {
public ApplicationController(UserService userService, ProjectService projectService, VersionService versionService, FlagService flagService, UserActionLogService userActionLogService, JobService jobService, SitemapService sitemapService, StatsService statsService, HttpServletRequest request) {
this.userService = userService;
this.projectService = projectService;
this.flagService = flagService;
@ -65,6 +71,7 @@ public class ApplicationController extends HangarController {
this.jobService = jobService;
this.sitemapService = sitemapService;
this.request = request;
this.statsService = statsService;
}
@RequestMapping("/")
@ -179,8 +186,29 @@ public class ApplicationController extends HangarController {
@Secured("ROLE_USER")
@RequestMapping("/admin/stats")
public ModelAndView showStats(@RequestParam(required = false) Object from, @RequestParam(required = false) Object to) {
return fillModel(new ModelAndView("users/admin/stats")); // TODO implement showStats request controller
public ModelAndView showStats(@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
ModelAndView mav = new ModelAndView("users/admin/stats");
if(from == null){
from = LocalDate.now().minus(30, ChronoUnit.DAYS);
}
if(to == null){
to = LocalDate.now();
}
if(to.isBefore(from)){
to = from;
}
List<Stats> stats = statsService.getStats(from, to);
mav.addObject("fromDate", from.toString());
mav.addObject("toDate", to.toString());
mav.addObject("days", statsService.getStatDays(stats));
mav.addObject("reviewData", statsService.getReviewStats(stats));
mav.addObject("uploadData", statsService.getUploadStats(stats));
mav.addObject("totalDownloadData", statsService.getTotalDownloadStats(stats));
mav.addObject("unsafeDownloadData", statsService.getUnsafeDownloadsStats(stats));
mav.addObject("openFlagsData", statsService.getFlagsOpenedStats(stats));
mav.addObject("closedFlagsData", statsService.getFlagsClosedStats(stats));
return fillModel(mav);
}
@GlobalPermission(NamedPermission.EDIT_ALL_USER_SETTINGS)

View File

@ -0,0 +1,29 @@
package io.papermc.hangar.db.dao;
import io.papermc.hangar.db.model.Stats;
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
@Repository
public interface ProjectStatsDao {
@SqlQuery("SELECT (SELECT COUNT(*) FROM project_version_reviews WHERE CAST(ended_at AS DATE) = day) AS reviews, " +
"(SELECT COUNT(*) FROM project_versions WHERE CAST(created_at AS DATE) = day) AS uploads, " +
"(SELECT COUNT(*) FROM project_versions_downloads_individual WHERE CAST(created_at AS DATE) = day) AS totalDownloads, " +
"(SELECT COUNT(*) FROM project_version_unsafe_downloads " +
" WHERE CAST(created_at AS DATE) = day) AS unsafeDownloads, " +
"(SELECT COUNT(*) FROM project_flags " +
" WHERE CAST(created_at AS DATE) <= day " +
" AND (CAST(resolved_at AS DATE) >= day OR resolved_at IS NULL)) AS flagsOpened, " +
"(SELECT COUNT(*) FROM project_flags WHERE CAST(resolved_at AS DATE) = day) AS flagsClosed, " +
"CAST(day AS DATE) " +
"FROM (SELECT generate_series(:startDate, :endDate, INTERVAL '1 DAY') AS day) dates " +
"ORDER BY day ASC; ")
@RegisterBeanMapper(Stats.class)
List<Stats> getStats(LocalDate startDate, LocalDate endDate);
}

View File

@ -0,0 +1,70 @@
package io.papermc.hangar.db.model;
import java.time.LocalDate;
public class Stats {
private long reviews;
private long uploads;
private long totalDownloads;
private long unsafeDownloads;
private long flagsOpened;
private long flagsClosed;
private LocalDate day;
public long getReviews() {
return reviews;
}
public void setReviews(long review) {
this.reviews = review;
}
public long getUploads() {
return uploads;
}
public void setUploads(long uploads) {
this.uploads = uploads;
}
public long getTotalDownloads() {
return totalDownloads;
}
public void setTotalDownloads(long totalDownloads) {
this.totalDownloads = totalDownloads;
}
public long getUnsafeDownloads() {
return unsafeDownloads;
}
public void setUnsafeDownloads(long unsafeDownloads) {
this.unsafeDownloads = unsafeDownloads;
}
public long getFlagsOpened() {
return flagsOpened;
}
public void setFlagsOpened(long flagsOpened) {
this.flagsOpened = flagsOpened;
}
public long getFlagsClosed() {
return flagsClosed;
}
public void setFlagsClosed(long flagsClosed) {
this.flagsClosed = flagsClosed;
}
public LocalDate getDay() {
return day;
}
public void setDay(LocalDate day) {
this.day = day;
}
}

View File

@ -0,0 +1,63 @@
package io.papermc.hangar.service;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.ProjectStatsDao;
import io.papermc.hangar.db.model.Stats;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
public class StatsService {
private final HangarDao<ProjectStatsDao> projectStatsDao;
@Autowired
public StatsService(HangarDao<ProjectStatsDao> projectStatsDao) {
this.projectStatsDao = projectStatsDao;
}
public Stream<LocalDate> getDaysBetween(LocalDate from, LocalDate to) {
return from.datesUntil(to.plusDays(1));
}
public List<Stats> getStats(LocalDate from, LocalDate to) {
return projectStatsDao.get().getStats(from, to);
}
public String getStatDays(List<Stats> stats) {
return getJsonListAsString(stats.stream().map(Stats::getDay));
}
public String getReviewStats(List<Stats> stats) {
return getJsonListAsString(stats.stream().map(Stats::getReviews));
}
public String getUploadStats(List<Stats> stats) {
return getJsonListAsString(stats.stream().map(Stats::getUploads));
}
public String getTotalDownloadStats(List<Stats> stats) {
return getJsonListAsString(stats.stream().map(Stats::getTotalDownloads));
}
public String getUnsafeDownloadsStats(List<Stats> stats) {
return getJsonListAsString(stats.stream().map(Stats::getUnsafeDownloads));
}
public String getFlagsOpenedStats(List<Stats> stats) {
return getJsonListAsString(stats.stream().map(Stats::getFlagsOpened));
}
public String getFlagsClosedStats(List<Stats> stats) {
return getJsonListAsString(stats.stream().map(Stats::getFlagsClosed));
}
public <T> String getJsonListAsString(Stream<T> stream) {
return stream.map(count -> "\"" + count + "\"").collect(Collectors.joining(", ", "[", "]"));
}
}

View File

@ -20,88 +20,89 @@
<#assign scriptsVar>
<script @CSPNonce.attr type="text/javascript" src="<@hangar.url "lib/chart.js/dist/Chart.min.js" />"></script>
<script @CSPNonce.attr>
$(function(){
var domChartReview = document.getElementById("chart-reviews");
var chartReviews = new Chart(domChartReview, {
responsive: true,
type: 'line',
data: {
labels: @Html(Json.stringify(Json.toJson(stats.map(_.day.toString)))),
datasets: [{
label: "Reviews",
backgroundColor: "cornflowerblue",
borderColor: "dodgerblue",
fill: false,
data: @Html(Json.stringify(Json.toJson(stats.map(_.reviews))))
}, {
label: "Uploads",
backgroundColor: "lightseagreen",
borderColor: "darkseagreen",
fill: false,
data: @Html(Json.stringify(Json.toJson(stats.map(_.uploads))))
}]
},
options: {
title: {
text: "Reviews"
}
$(function(){
var timeFrame = ${days}
var domChartReview = document.getElementById("chart-reviews");
var chartReviews = new Chart(domChartReview, {
responsive: true,
type: 'line',
data: {
labels: timeFrame,
datasets: [{
label: "Reviews",
backgroundColor: "cornflowerblue",
borderColor: "dodgerblue",
fill: false,
data: ${reviewData}
}, {
label: "Uploads",
backgroundColor: "lightseagreen",
borderColor: "darkseagreen",
fill: false,
data: ${uploadData}
}]
},
options: {
title: {
text: "Reviews"
}
});
var domChartDownload = document.getElementById("chart-downloads");
var chartDownloads = new Chart(domChartDownload, {
responsive: true,
type: 'line',
data: {
labels: @Html(Json.stringify(Json.toJson(stats.map(_.day.toString).takeRight(30)))),
datasets: [{
label: "Total Downloads",
backgroundColor: "cornflowerblue",
borderColor: "dodgerblue",
fill: false,
data: @Html(Json.stringify(Json.toJson(stats.map(_.totalDownloads).takeRight(30))))
}, {
label: "Unsafe Downloads",
backgroundColor: "lightseagreen",
borderColor: "darkseagreen",
fill: false,
data: @Html(Json.stringify(Json.toJson(stats.map(_.unsafeDownloads).takeRight(30))))
}]
},
options: {
title: {
text: "Downloads"
}
}
});
var domChartFlags = document.getElementById("chart-flags");
var chartFlags = new Chart(domChartFlags, {
responsive: true,
type: 'line',
data: {
labels: @Html(Json.stringify(Json.toJson(stats.map(_.day.toString)))),
datasets: [{
label: "Open flags",
backgroundColor: "cornflowerblue",
borderColor: "dodgerblue",
fill: false,
data: @Html(Json.stringify(Json.toJson(stats.map(_.flagsOpened))))
}, {
label: "Closed flags",
backgroundColor: "lightseagreen",
borderColor: "darkseagreen",
fill: false,
data: @Html(Json.stringify(Json.toJson(stats.map(_.flagsClosed))))
}]
},
options: {
title: {
text: "Flags"
}
}
});
}
});
var domChartDownload = document.getElementById("chart-downloads");
var chartDownloads = new Chart(domChartDownload, {
responsive: true,
type: 'line',
data: {
labels: timeFrame,
datasets: [{
label: "Total Downloads",
backgroundColor: "cornflowerblue",
borderColor: "dodgerblue",
fill: false,
data: ${totalDownloadData}
}, {
label: "Unsafe Downloads",
backgroundColor: "lightseagreen",
borderColor: "darkseagreen",
fill: false,
data: ${unsafeDownloadData}
}]
},
options: {
title: {
text: "Downloads"
}
}
});
var domChartFlags = document.getElementById("chart-flags");
var chartFlags = new Chart(domChartFlags, {
responsive: true,
type: 'line',
data: {
labels: timeFrame,
datasets: [{
label: "Opened flags", // 'Open flags' is a bit of a misleading name
backgroundColor: "cornflowerblue",
borderColor: "dodgerblue",
fill: false,
data: ${openFlagsData}
}, {
label: "Closed flags",
backgroundColor: "lightseagreen",
borderColor: "darkseagreen",
fill: false,
data: ${closedFlagsData}
}]
},
options: {
title: {
text: "Flags"
}
}
});
});
</script>
<script @CSPNonce.attr type="text/javascript" src="<@hangar.url "javascripts/stats.js" />"></script>
</#assign>
@ -113,11 +114,11 @@
<div class="form-inline">
<div class="form-group">
<label for="fromDate">From:</label>
<input id="fromDate" type="date" class="form-control" value="@fromTime.toString" max="@LocalDate.now().minus(1, ChronoUnit.DAYS)">
<input id="fromDate" type="date" class="form-control" max="@LocalDate.now().minus(1, ChronoUnit.DAYS)" <#if fromDate??> value="${fromDate}"</#if>>
</div>
<div class="form-group">
<label for="toDate">To:</label>
<input id="toDate" type="date" class="form-control" value="@toTime.toString" max="@LocalDate.now().toString">
<input id="toDate" type="date" class="form-control" max="@LocalDate.now().toString" <#if toDate??> value="${toDate}"</#if>>
</div>
<button id="dateGoButton" class="btn btn-default">Go</button>
</div>