View options added to the query

This commit is contained in:
Risto Lahtela 2021-01-12 17:11:12 +02:00
parent 5c102458ae
commit c7ed844c76
11 changed files with 267 additions and 69 deletions

View File

@ -42,6 +42,7 @@ public class Formatters {
private final DayFormatter dayLongFormatter;
private final SecondFormatter secondLongFormatter;
private final ClockFormatter clockLongFormatter;
private final JavascriptDateFormatter javascriptDateFormatter;
private final ISO8601NoClockFormatter iso8601NoClockLongFormatter;
private final ISO8601NoClockTZIndependentFormatter iso8601NoClockTZIndependentFormatter;
@ -57,6 +58,7 @@ public class Formatters {
dayLongFormatter = new DayFormatter(config, locale);
clockLongFormatter = new ClockFormatter(config, locale);
secondLongFormatter = new SecondFormatter(config, locale);
javascriptDateFormatter = new JavascriptDateFormatter(config, locale);
iso8601NoClockLongFormatter = new ISO8601NoClockFormatter(config, locale);
iso8601NoClockTZIndependentFormatter = new ISO8601NoClockTZIndependentFormatter();
@ -109,6 +111,10 @@ public class Formatters {
return iso8601NoClockFormatter;
}
public Formatter<Long> javascriptDateFormatterLong() {
return javascriptDateFormatter;
}
public Formatter<Long> iso8601NoClockLong() {
return iso8601NoClockLongFormatter;
}

View File

@ -0,0 +1,37 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.formatting.time;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.locale.Locale;
/**
* Formats epoch milliseconds to the date format Javascript Date constructor expects.
*
* @author Rsl1122
*/
public class JavascriptDateFormatter extends DateFormatter {
public JavascriptDateFormatter(PlanConfig config, Locale locale) {
super(config, locale);
}
@Override
public String apply(Long epochMs) {
return format(epochMs, "dd/MM/yyyy kk:mm");
}
}

View File

@ -16,6 +16,8 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
@ -23,7 +25,7 @@ import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.storage.database.queries.filter.Filter;
import com.djrapitops.plan.storage.database.queries.filter.QueryFilters;
import com.djrapitops.plan.utilities.java.Maps;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -31,17 +33,21 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Singleton
public class FiltersJSONResolver implements Resolver {
private final QueryFilters filters;
private final Formatters formatters;
@Inject
public FiltersJSONResolver(
QueryFilters filters
QueryFilters filters,
Formatters formatters
) {
this.filters = filters;
this.formatters = formatters;
}
@Override
@ -58,21 +64,64 @@ public class FiltersJSONResolver implements Resolver {
private Response getResponse() {
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(Maps.builder(String.class, Object.class)
.put("filters", serializeFilters())
.build())
.build();
.setJSONContent(new FilterResponseJSON(
filters.getFilters(),
new ViewJSON(formatters)
)).build();
}
private List<Map<String, Object>> serializeFilters() {
List<Map<String, Object>> filterList = new ArrayList<>();
for (Map.Entry<String, Filter> entry : filters.getFilters().entrySet()) {
filterList.add(Maps.builder(String.class, Object.class)
.put("kind", entry.getKey())
.put("options", entry.getValue().getOptions())
.put("expectedParameters", entry.getValue().getExpectedParameters())
.build());
/**
* JSON serialization class.
*/
static class FilterResponseJSON {
final List<FilterJSON> filters;
final ViewJSON view;
public FilterResponseJSON(Map<String, Filter> filtersByKind, ViewJSON view) {
this.filters = new ArrayList<>();
for (Map.Entry<String, Filter> entry : filtersByKind.entrySet()) {
filters.add(new FilterJSON(entry.getKey(), entry.getValue()));
}
this.view = view;
}
}
/**
* JSON serialization class.
*/
static class FilterJSON {
final String kind;
final Map<String, Object> options;
final String[] expectedParameters;
public FilterJSON(String kind, Filter filter) {
this.kind = kind;
this.options = filter.getOptions();
this.expectedParameters = filter.getExpectedParameters();
}
}
/**
* JSON serialization class.
*/
static class ViewJSON {
final String afterDate;
final String afterTime;
final String beforeDate;
final String beforeTime;
public ViewJSON(Formatters formatters) {
long now = System.currentTimeMillis();
long monthAgo = now - TimeUnit.DAYS.toMillis(30);
Formatter<Long> formatter = formatters.javascriptDateFormatterLong();
String[] after = StringUtils.split(formatter.apply(monthAgo), " ");
String[] before = StringUtils.split(formatter.apply(now), " ");
this.afterDate = after[0];
this.afterTime = after[1];
this.beforeDate = before[0];
this.beforeTime = before[1];
}
return filterList;
}
}

View File

@ -35,11 +35,14 @@ import com.djrapitops.plan.storage.database.queries.filter.FilterQuery;
import com.djrapitops.plan.storage.database.queries.filter.QueryFilters;
import com.djrapitops.plan.storage.database.queries.objects.playertable.QueryTablePlayersQuery;
import com.djrapitops.plan.utilities.java.Maps;
import com.google.gson.Gson;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.net.URLDecoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
@Singleton
@ -81,6 +84,7 @@ public class QueryJSONResolver implements Resolver {
private Response getResponse(Request request) {
String q = request.getQuery().get("q").orElseThrow(() -> new BadRequestException("'q' parameter not set (expecting json array)"));
String view = request.getQuery().get("view").orElseThrow(() -> new BadRequestException("'view' parameter not set (expecting json object {afterDate, afterTime, beforeDate, beforeTime})"));
try {
q = URLDecoder.decode(q, "UTF-8");
List<FilterQuery> queries = FilterQuery.parse(q);
@ -90,23 +94,30 @@ public class QueryJSONResolver implements Resolver {
.put("path", result.getResultPath())
.build();
if (!result.isEmpty()) {
json.put("data", getDataFor(result.getResultUUIDs()));
json.put("data", getDataFor(result.getResultUUIDs(), view));
}
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(json)
.build();
} catch (ParseException e) {
throw new BadRequestException("'view' date format was incorrect (expecting afterDate dd/mm/yyyy, afterTime hh:mm, beforeDate dd/mm/yyyy, beforeTime hh:mm}): " + e.getMessage());
} catch (IOException e) {
throw new BadRequestException("Failed to parse json: '" + q + "'" + e.getMessage());
}
}
private Map<String, Object> getDataFor(Set<UUID> playerUUIDs) {
private Map<String, Object> getDataFor(Set<UUID> playerUUIDs, String view) throws ParseException {
FiltersJSONResolver.ViewJSON viewJSON = new Gson().fromJson(view, FiltersJSONResolver.ViewJSON.class);
SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy kk:mm");
long after = dateFormat.parse(viewJSON.afterDate + " " + viewJSON.afterTime).getTime();
long before = dateFormat.parse(viewJSON.beforeDate + " " + viewJSON.beforeTime).getTime();
Database database = dbSystem.getDatabase();
return Maps.builder(String.class, Object.class)
.put("players", new PlayersTableJSONCreator(
database.query(new QueryTablePlayersQuery(playerUUIDs, System.currentTimeMillis(), config.get(TimeSettings.ACTIVE_PLAY_THRESHOLD))),
database.query(new QueryTablePlayersQuery(playerUUIDs, after, before, config.get(TimeSettings.ACTIVE_PLAY_THRESHOLD))),
Collections.emptyMap(),
config.get(DisplaySettings.OPEN_PLAYER_LINKS_IN_NEW_TAB),
formatters, locale

View File

@ -17,6 +17,7 @@
package com.djrapitops.plan.storage.database.queries.filter;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import com.djrapitops.plan.storage.database.queries.filter.filters.AllPlayersFilter;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -32,9 +33,14 @@ import java.util.*;
public class QueryFilters {
private final Map<String, Filter> filters;
private final AllPlayersFilter allPlayersFilter;
@Inject
public QueryFilters(Set<Filter> filters) {
public QueryFilters(
Set<Filter> filters,
AllPlayersFilter allPlayersFilter
) {
this.allPlayersFilter = allPlayersFilter;
this.filters = new HashMap<>();
put(filters);
}
@ -58,6 +64,7 @@ public class QueryFilters {
*/
public Filter.Result apply(List<FilterQuery> filterQueries) {
Filter.Result current = null;
if (filterQueries.isEmpty()) return allPlayersFilter.apply(null);
for (FilterQuery filterQuery : filterQueries) {
current = apply(current, filterQuery);
if (current != null && current.isEmpty()) break;

View File

@ -0,0 +1,58 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.storage.database.queries.filter.filters;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.filter.Filter;
import com.djrapitops.plan.storage.database.queries.filter.FilterQuery;
import com.djrapitops.plan.storage.database.queries.objects.UserIdentifierQueries;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Set;
import java.util.UUID;
/**
* Special filter only used in cases where no filters are specified.
*
* @author Rsl1122
*/
@Singleton
public class AllPlayersFilter implements Filter {
private final DBSystem dbSystem;
@Inject
public AllPlayersFilter(DBSystem dbSystem) {
this.dbSystem = dbSystem;
}
@Override
public String getKind() {
return "all";
}
@Override
public String[] getExpectedParameters() {
return new String[0];
}
@Override
public Set<UUID> getMatchingUUIDs(FilterQuery query) {
return dbSystem.getDatabase().query(UserIdentifierQueries.fetchAllPlayerUUIDs());
}
}

View File

@ -40,10 +40,10 @@ public abstract class DateRangeFilter implements Filter {
@Override
public String[] getExpectedParameters() {
return new String[]{
"dateAfter",
"timeAfter",
"dateBefore",
"timeBefore"
"afterDate",
"afterTime",
"beforeDate",
"beforeTime"
};
}
@ -61,11 +61,11 @@ public abstract class DateRangeFilter implements Filter {
}
protected long getAfter(FilterQuery query) {
return getTime(query, "dateAfter", "timeAfter");
return getTime(query, "afterDate", "afterTime");
}
protected long getBefore(FilterQuery query) {
return getTime(query, "dateBefore", "timeBefore");
return getTime(query, "beforeDate", "beforeTime");
}
private long getTime(FilterQuery query, String dateKey, String timeKey) {

View File

@ -46,19 +46,21 @@ import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
public class QueryTablePlayersQuery implements Query<List<TablePlayer>> {
private final Collection<UUID> playerUUIDs;
private final long date;
private final long afterDate;
private final long beforeDate;
private final long activeMsThreshold;
/**
* Create a new query.
*
* @param playerUUIDs UUIDs of the players in the query
* @param date Date used for Activity Index calculation
* @param beforeDate Date used for Activity Index calculation
* @param activeMsThreshold Playtime threshold for Activity Index calculation
*/
public QueryTablePlayersQuery(Collection<UUID> playerUUIDs, long date, long activeMsThreshold) {
public QueryTablePlayersQuery(Collection<UUID> playerUUIDs, long afterDate, long beforeDate, long activeMsThreshold) {
this.playerUUIDs = playerUUIDs;
this.date = date;
this.afterDate = afterDate;
this.beforeDate = beforeDate;
this.activeMsThreshold = activeMsThreshold;
}
@ -88,7 +90,9 @@ public class QueryTablePlayersQuery implements Query<List<TablePlayer>> {
"COUNT(1) as count," +
"SUM(" + SessionsTable.SESSION_END + '-' + SessionsTable.SESSION_START + ") as playtime" +
FROM + SessionsTable.TABLE_NAME + " s" +
WHERE + "s." + SessionsTable.USER_UUID +
WHERE + "s." + SessionsTable.SESSION_START + ">=?" +
AND + "s." + SessionsTable.SESSION_END + "<=?" +
AND + "s." + SessionsTable.USER_UUID +
uuidsInSet +
GROUP_BY + "s." + SessionsTable.USER_UUID;
@ -114,7 +118,9 @@ public class QueryTablePlayersQuery implements Query<List<TablePlayer>> {
return db.query(new QueryStatement<List<TablePlayer>>(selectBaseUsers, 1000) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
NetworkActivityIndexQueries.setSelectActivityIndexSQLParameters(statement, 1, activeMsThreshold, date);
statement.setLong(1, afterDate);
statement.setLong(2, beforeDate);
NetworkActivityIndexQueries.setSelectActivityIndexSQLParameters(statement, 3, activeMsThreshold, beforeDate);
}
@Override
@ -129,7 +135,7 @@ public class QueryTablePlayersQuery implements Query<List<TablePlayer>> {
.lastSeen(set.getLong("last_seen"))
.sessionCount(set.getInt("count"))
.playtime(set.getLong("playtime"))
.activityIndex(new ActivityIndex(set.getDouble("activity_index"), date));
.activityIndex(new ActivityIndex(set.getDouble("activity_index"), beforeDate));
if (set.getBoolean(UserInfoTable.BANNED)) {
player.banned();
}

View File

@ -4,11 +4,11 @@ let filterCount = 0;
id: "DOM id",
options...
}*/
const filterView = {
dateAfter: null,
timeAfter: null,
dateBefore: null,
timeBefore: null
let filterView = {
afterDate: null,
afterTime: null,
beforeDate: null,
beforeTime: null
};
const filterQuery = [];
@ -102,10 +102,10 @@ class BetweenDateFilter extends Filter {
super(kind);
this.id = id;
this.label = label;
this.dateAfter = options.after[0];
this.timeAfter = options.after[1];
this.dateBefore = options.before[0];
this.timeBefore = options.before[1];
this.afterDate = options.after[0];
this.afterTime = options.after[1];
this.beforeDate = options.before[0];
this.beforeTime = options.before[1];
}
render(filterCount) {
@ -116,20 +116,20 @@ class BetweenDateFilter extends Filter {
`<div id="${id}" class="mt-2 input-group input-row">` +
`<div class="col-3"><div class="input-group mb-2">` +
`<div class="input-group-prepend"><div class="input-group-text"><i class="far fa-calendar"></i></div></div>` +
`<input id="${id}-afterdate" onkeyup="setFilterOption('${id}', '${id}-afterdate', 'dateAfter', isValidDate, correctDate)" class="form-control" placeholder="${this.dateAfter}" type="text">` +
`<input id="${id}-afterdate" onkeyup="setFilterOption('${id}', '${id}-afterdate', 'afterDate', isValidDate, correctDate)" class="form-control" placeholder="${this.afterDate}" type="text">` +
`</div></div>` +
`<div class="col-2"><div class="input-group mb-2">` +
`<div class="input-group-prepend"><div class="input-group-text"><i class="far fa-clock"></i></div></div>` +
`<input id="${id}-aftertime" onkeyup="setFilterOption('${id}', '${id}-aftertime', 'timeAfter', isValidTime, correctTime)" class="form-control" placeholder="${this.timeAfter}" type="text">` +
`<input id="${id}-aftertime" onkeyup="setFilterOption('${id}', '${id}-aftertime', 'afterTime', isValidTime, correctTime)" class="form-control" placeholder="${this.afterTime}" type="text">` +
`</div></div>` +
`<div class="col-auto"><label class="mt-2 mb-0" for="inlineFormCustomSelectPref">&</label></div>` +
`<div class="col-3"><div class="input-group mb-2">` +
`<div class="input-group-prepend"><div class="input-group-text"><i class="far fa-calendar"></i></div></div>` +
`<input id="${id}-beforedate" onkeyup="setFilterOption('${id}', '${id}-beforedate', 'dateBefore', isValidDate, correctDate)" class="form-control" placeholder="${this.dateBefore}" type="text">` +
`<input id="${id}-beforedate" onkeyup="setFilterOption('${id}', '${id}-beforedate', 'beforeDate', isValidDate, correctDate)" class="form-control" placeholder="${this.beforeDate}" type="text">` +
`</div></div>` +
`<div class="col-2"><div class="input-group mb-2">` +
`<div class="input-group-prepend"><div class="input-group-text"><i class="far fa-clock"></i></div></div>` +
`<input id="${id}-beforetime" onkeyup="setFilterOption('${id}', '${id}-beforetime', 'timeBefore', isValidTime, correctTime)" class="form-control" placeholder="${this.timeBefore}" type="text">` +
`<input id="${id}-beforetime" onkeyup="setFilterOption('${id}', '${id}-beforetime', 'beforeTime', isValidTime, correctTime)" class="form-control" placeholder="${this.beforeTime}" type="text">` +
`</div></div>` +
`</div>`
);
@ -139,10 +139,10 @@ class BetweenDateFilter extends Filter {
return {
kind: this.kind,
parameters: {
dateAfter: this.dateAfter,
timeAfter: this.timeAfter,
dateBefore: this.dateBefore,
timeBefore: this.timeBefore
afterDate: this.afterDate,
afterTime: this.afterTime,
beforeDate: this.beforeDate,
beforeTime: this.beforeTime
}
}
}
@ -274,7 +274,7 @@ function performQuery() {
if (json) console.log(json);
if (error) console.error(error);
renderDataResultScreen();
renderDataResultScreen(json.data.players.data.length);
$('.player-table').DataTable({
responsive: true,
@ -282,24 +282,41 @@ function performQuery() {
data: json.data.players.data,
order: [[5, "desc"]]
})
const activityIndexHeader = document.querySelector("#DataTables_Table_0 thead th:nth-of-type(2)");
const lastSeenHeader = document.querySelector("#DataTables_Table_0 thead th:nth-of-type(6)");
activityIndexHeader.innerHTML += ` (${filterView.beforeDate})`
lastSeenHeader.innerHTML += ` (view)`
});
}
function renderDataResultScreen() {
document.querySelector('#content .tab').innerHTML +=
`<div class="row">
<div class="col-xs-12 col-sm-12 col-lg-11">
<div class="card shadow mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover player-table dataTable">
<tr>
<td>Loading..</td>
</tr>
</table>
function renderDataResultScreen(resultCount) {
document.querySelector('#content .tab').innerHTML =
`<div class="container-fluid mt-4">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800"><i class="sidebar-toggler fa fa-fw fa-bars"></i>Plan &middot;
Query Results</h1>
<p class="mb-0 text-gray-800">(matched ${resultCount} players)</p>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-lg-11">
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold col-black" title=" ${filterView.afterDate} ${filterView.afterTime} - ${filterView.beforeDate} ${filterView.beforeTime}"><i
class="fas fa-fw fa-users col-black"></i>
View: ${filterView.afterDate} - ${filterView.beforeDate}</h6>
</div>
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover player-table dataTable">
<tr>
<td>Loading..</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>`;
</div>`;
}

View File

@ -43,7 +43,7 @@ function openPage() {
// Prepare tabs for display
content.style.transform = "translate3d(0px,0px,0)";
content.style.width = (tabCount * 100) + "%";
content.style.width = (Math.max(100, tabCount * 100)) + "%";
content.style.opacity = "1";
for (let tab of tabs) {
tab.style.width = (100 / tabCount) + "%";

View File

@ -93,7 +93,7 @@
<div class="input-group-text"><i class="far fa-calendar"></i></div>
</div>
<input class="form-control" id="viewFromDateField"
onkeyup="setFilterOption('view', 'viewFromDateField', 'dateBefore', isValidDate, correctDate)"
onkeyup="setFilterOption('view', 'viewFromDateField', 'beforeDate', isValidDate, correctDate)"
placeholder="31/12/2016"
type="text">
</div>
@ -105,7 +105,7 @@
<div class="input-group-text"><i class="far fa-clock"></i></div>
</div>
<input class="form-control" id="viewFromTimeField"
onkeyup="setFilterOption('view', 'viewFromTimeField', 'timeBefore', isValidTime, correctTime)"
onkeyup="setFilterOption('view', 'viewFromTimeField', 'beforeTime', isValidTime, correctTime)"
placeholder="23:59"
type="text">
</div>
@ -119,7 +119,7 @@
<div class="input-group-text"><i class="far fa-calendar"></i></div>
</div>
<input class="form-control" id="viewToDateField"
onkeyup="setFilterOption('view', 'viewToDateField', 'dateAfter', isValidDate, correctDate)"
onkeyup="setFilterOption('view', 'viewToDateField', 'afterDate', isValidDate, correctDate)"
placeholder="23/03/2020"
type="text">
</div>
@ -130,7 +130,7 @@
<div class="input-group-text"><i class="far fa-clock"></i></div>
</div>
<input class="form-control" id="viewToTimeField"
onkeyup="setFilterOption('view', 'viewToTimeField', 'timeAfter', isValidTime, correctTime)"
onkeyup="setFilterOption('view', 'viewToTimeField', 'afterTime', isValidTime, correctTime)"
placeholder="21:26"
type="text">
</div>
@ -325,6 +325,13 @@
jsonRequest("./v1/filters", function (json, error) {
filters.push(...json.filters);
filterView = json.view;
document.getElementById('viewFromDateField').setAttribute('placeholder', json.view.afterDate);
document.getElementById('viewFromTimeField').setAttribute('placeholder', json.view.afterTime);
document.getElementById('viewToDateField').setAttribute('placeholder', json.view.beforeDate);
document.getElementById('viewToTimeField').setAttribute('placeholder', json.view.beforeTime);
let filterElements = '';
for (let i = 0; i < filters.length; i++) {
filterElements += createFilterSelector('#filters', i, filters[i]);