mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-03-31 16:00:39 +08:00
Port Admin/stats (#106)
Co-authored-by: KennyTV <jahnke.nassim@gmail.com>
This commit is contained in:
parent
8e97d55516
commit
3575848ab5
@ -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);
|
||||
|
@ -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)
|
||||
|
29
src/main/java/io/papermc/hangar/db/dao/ProjectStatsDao.java
Normal file
29
src/main/java/io/papermc/hangar/db/dao/ProjectStatsDao.java
Normal 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);
|
||||
|
||||
}
|
70
src/main/java/io/papermc/hangar/db/model/Stats.java
Normal file
70
src/main/java/io/papermc/hangar/db/model/Stats.java
Normal 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;
|
||||
}
|
||||
}
|
63
src/main/java/io/papermc/hangar/service/StatsService.java
Normal file
63
src/main/java/io/papermc/hangar/service/StatsService.java
Normal 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(", ", "[", "]"));
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user