Playerbase overview numbers to network page

This commit is contained in:
Rsl1122 2019-08-15 14:24:30 +03:00
parent d2c70186ec
commit 986a753918
22 changed files with 978 additions and 144 deletions

View File

@ -0,0 +1,415 @@
/*
* 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.db.access.queries.analysis;
import com.djrapitops.plan.data.store.mutators.ActivityIndex;
import com.djrapitops.plan.db.access.Query;
import com.djrapitops.plan.db.access.QueryStatement;
import com.djrapitops.plan.db.sql.tables.SessionsTable;
import com.djrapitops.plan.db.sql.tables.UsersTable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.djrapitops.plan.db.sql.parsing.Sql.*;
/**
* Queries for Activity Index that attempts to gain insight into player activity levels.
* <p>
* Old formula for activity index was not linear and difficult to turn into a query due to conditional multipliers.
* Thus a new formula was written.
* <p>
* {@code T} - Time played after someone is considered active on a particular week
* {@code t1, t2, t3} - Time played that week
* <p>
* Activity index takes into account last 3 weeks.
* <p>
* Activity for a single week is calculated using {@code A(t) = (1 / (pi/2 * (t/T) + 1))}.
* A(t) is based on function f(x) = 1 / (x + 1), which has property f(0) = 1, decreasing from there, but not in a straight line.
* You can see the function plotted here https://www.wolframalpha.com/input/?i=1+%2F+(x%2B1)+from+-1+to+2
* <p>
* To fine tune the curve pi/2 is used since it felt like a good curve.
* <p>
* Activity index A is calculated by using the formula:
* {@code A = 5 - 5 * [A(t1) + A(t2) + A(t3)] / 3}
* <p>
* Plot for A and limits
* https://www.wolframalpha.com/input/?i=plot+y+%3D+5+-+5+*+(1+%2F+(pi%2F2+*+x%2B1))+and+y+%3D1+and+y+%3D+2+and+y+%3D+3+and+y+%3D+3.75+from+-0.5+to+3
* <p>
* New Limits for A would thus be
* {@code < 1: Inactive}
* {@code > 1: Irregular}
* {@code > 2: Regular}
* {@code > 3: Active}
* {@code > 3.75: Very Active}
*
* @author Rsl1122
*/
public class NetworkActivityIndexQueries {
private NetworkActivityIndexQueries() {
// Static method class
}
public static Query<Integer> fetchRegularPlayerCount(long date, long playtimeThreshold) {
return fetchActivityGroupCount(date, playtimeThreshold, ActivityIndex.REGULAR, 5.1);
}
private static String selectActivityIndexSQL() {
String selectActivePlaytimeSQL = SELECT +
SessionsTable.USER_UUID +
",SUM(" +
SessionsTable.SESSION_END + '-' + SessionsTable.SESSION_START + '-' + SessionsTable.AFK_TIME +
") as active_playtime" +
FROM + SessionsTable.TABLE_NAME +
WHERE + SessionsTable.SESSION_START + ">=?" +
AND + SessionsTable.SESSION_END + "<=?" +
GROUP_BY + SessionsTable.USER_UUID;
String selectThreeWeeks = selectActivePlaytimeSQL + UNION + selectActivePlaytimeSQL + UNION + selectActivePlaytimeSQL;
return SELECT +
"5.0 - 5.0 * AVG(1 / (?/2 * (q1.active_playtime/?) +1)) as activity_index," +
"q1." + SessionsTable.USER_UUID +
FROM + '(' + selectThreeWeeks + ") q1" +
GROUP_BY + "q1." + SessionsTable.USER_UUID;
}
private static void setSelectActivityIndexSQLParameters(PreparedStatement statement, int index, long playtimeThreshold, long date) throws SQLException {
statement.setDouble(index, Math.PI);
statement.setLong(index + 1, playtimeThreshold);
statement.setLong(index + 2, date - TimeUnit.DAYS.toMillis(7L));
statement.setLong(index + 3, date);
statement.setLong(index + 4, date - TimeUnit.DAYS.toMillis(14L));
statement.setLong(index + 5, date - TimeUnit.DAYS.toMillis(7L));
statement.setLong(index + 6, date - TimeUnit.DAYS.toMillis(21L));
statement.setLong(index + 7, date - TimeUnit.DAYS.toMillis(14L));
}
public static Query<Integer> fetchActivityGroupCount(long date, long playtimeThreshold, double above, double below) {
String selectActivityIndex = selectActivityIndexSQL();
String selectIndexes = SELECT + "COALESCE(activity_index, 0) as activity_index" +
FROM + UsersTable.TABLE_NAME + " u" +
LEFT_JOIN + '(' + selectActivityIndex + ") q2 on q2." + SessionsTable.USER_UUID + "=u." + UsersTable.USER_UUID +
WHERE + "u." + UsersTable.REGISTERED + "<=?";
String selectActivePlayerCount = SELECT + "COUNT(1) as count" +
FROM + '(' + selectIndexes + ") i" +
WHERE + "i.activity_index>=?" +
AND + "i.activity_index<?";
return new QueryStatement<Integer>(selectActivePlayerCount) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
setSelectActivityIndexSQLParameters(statement, 1, playtimeThreshold, date);
statement.setLong(9, date);
statement.setDouble(10, above);
statement.setDouble(11, below);
}
@Override
public Integer processResults(ResultSet set) throws SQLException {
return set.next() ? set.getInt("count") : 0;
}
};
}
public static Query<Map<String, Integer>> fetchActivityIndexGroupingsOn(long date, long threshold) {
return db -> {
Map<String, Integer> groups = new HashMap<>();
groups.put("Very Active", db.query(fetchActivityGroupCount(date, threshold, ActivityIndex.VERY_ACTIVE, 5.1)));
groups.put("Active", db.query(fetchActivityGroupCount(date, threshold, ActivityIndex.ACTIVE, ActivityIndex.VERY_ACTIVE)));
groups.put("Regular", db.query(fetchActivityGroupCount(date, threshold, ActivityIndex.REGULAR, ActivityIndex.ACTIVE)));
groups.put("Irregular", db.query(fetchActivityGroupCount(date, threshold, ActivityIndex.IRREGULAR, ActivityIndex.REGULAR)));
groups.put("Inactive", db.query(fetchActivityGroupCount(date, threshold, -0.1, ActivityIndex.IRREGULAR)));
return groups;
};
}
public static Query<Integer> countNewPlayersTurnedRegular(long after, long before, Long threshold) {
String selectActivityIndex = selectActivityIndexSQL();
String selectActivePlayerCount = SELECT + "COUNT(1) as count" +
FROM + '(' + selectActivityIndex + ") q2" +
INNER_JOIN + UsersTable.TABLE_NAME + " u on u." + UsersTable.USER_UUID + "=q2." + SessionsTable.USER_UUID +
WHERE + "u." + UsersTable.REGISTERED + ">=?" +
AND + "u." + UsersTable.REGISTERED + "<=?" +
AND + "q2.activity_index>=?" +
AND + "q2.activity_index<?";
return new QueryStatement<Integer>(selectActivePlayerCount) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
setSelectActivityIndexSQLParameters(statement, 1, threshold, before);
statement.setLong(9, after);
statement.setLong(10, before);
statement.setDouble(11, ActivityIndex.REGULAR);
statement.setDouble(12, 5.1);
}
@Override
public Integer processResults(ResultSet set) throws SQLException {
return set.next() ? set.getInt("count") : 0;
}
};
}
/**
* @param start Start of the tracking, those regular will be counted here.
* @param end End of the tracking, those inactive will be count here.
* @param threshold Playtime threshold
* @return Query how many players went from regular to inactive in a span of time.
*/
public static Query<Integer> countRegularPlayersTurnedInactive(long start, long end, Long threshold) {
String selectActivityIndex = selectActivityIndexSQL();
String selectActivePlayerCount = SELECT + "COUNT(1) as count" +
FROM + '(' + selectActivityIndex + ") q2" +
// Join two select activity index queries together to query Regular and Inactive players
INNER_JOIN + '(' + selectActivityIndex.replace("q1", "q3") + ") q4" +
" on q2." + SessionsTable.USER_UUID + "=q4." + SessionsTable.USER_UUID +
WHERE + "q2.activity_index>=?" +
AND + "q2.activity_index<?" +
AND + "q4.activity_index>=?" +
AND + "q4.activity_index<?";
return new QueryStatement<Integer>(selectActivePlayerCount) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
setSelectActivityIndexSQLParameters(statement, 1, threshold, end);
setSelectActivityIndexSQLParameters(statement, 9, threshold, start);
statement.setDouble(17, ActivityIndex.REGULAR);
statement.setDouble(18, 5.1);
statement.setDouble(19, -0.1);
statement.setDouble(20, ActivityIndex.IRREGULAR);
}
@Override
public Integer processResults(ResultSet set) throws SQLException {
return set.next() ? set.getInt("count") : 0;
}
};
}
public static Query<Long> averagePlaytimePerRegularPlayer(long after, long before, Long threshold) {
return database -> {
// INNER JOIN limits the users to only those that are regular
String selectPlaytimePerPlayer = SELECT +
"p." + SessionsTable.USER_UUID + "," +
"SUM(p." + SessionsTable.SESSION_END + "-p." + SessionsTable.SESSION_START + ") as playtime" +
FROM + SessionsTable.TABLE_NAME + " p" +
INNER_JOIN + '(' + selectActivityIndexSQL() + ") q2 on q2." + SessionsTable.USER_UUID + "=p." + SessionsTable.USER_UUID +
WHERE + "p." + SessionsTable.SESSION_END + "<=?" +
AND + "p." + SessionsTable.SESSION_START + ">=?" +
AND + "q2.activity_index>=?" +
AND + "q2.activity_index<?" +
GROUP_BY + "p." + SessionsTable.USER_UUID;
String selectAverage = SELECT + "AVG(playtime) as average" + FROM + '(' + selectPlaytimePerPlayer + ") q1";
return database.query(new QueryStatement<Long>(selectAverage, 100) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
setSelectActivityIndexSQLParameters(statement, 1, threshold, before);
statement.setLong(9, before);
statement.setLong(10, after);
statement.setDouble(11, ActivityIndex.REGULAR);
statement.setDouble(12, 5.1);
}
@Override
public Long processResults(ResultSet set) throws SQLException {
return set.next() ? set.getLong("average") : 0;
}
});
};
}
public static Query<Long> averageSessionLengthPerRegularPlayer(long after, long before, Long threshold) {
return database -> {
// INNER JOIN limits the users to only those that are regular
String selectSessionLengthPerPlayer = SELECT +
"p." + SessionsTable.USER_UUID + "," +
"p." + SessionsTable.SESSION_END + "-p." + SessionsTable.SESSION_START + " as length" +
FROM + SessionsTable.TABLE_NAME + " p" +
INNER_JOIN + '(' + selectActivityIndexSQL() + ") q2 on q2." + SessionsTable.USER_UUID + "=p." + SessionsTable.USER_UUID +
WHERE + "p." + SessionsTable.SESSION_END + "<=?" +
AND + "p." + SessionsTable.SESSION_START + ">=?" +
AND + "q2.activity_index>=?" +
AND + "q2.activity_index<?";
String selectAverage = SELECT + "AVG(length) as average" + FROM + '(' + selectSessionLengthPerPlayer + ") q1";
return database.query(new QueryStatement<Long>(selectAverage, 100) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
setSelectActivityIndexSQLParameters(statement, 1, threshold, before);
statement.setLong(9, before);
statement.setLong(10, after);
statement.setDouble(11, ActivityIndex.REGULAR);
statement.setDouble(12, 5.1);
}
@Override
public Long processResults(ResultSet set) throws SQLException {
return set.next() ? set.getLong("average") : 0;
}
});
};
}
public static Query<Long> averageAFKPerRegularPlayer(long after, long before, Long threshold) {
return database -> {
// INNER JOIN limits the users to only those that are regular
String selectPlaytimePerPlayer = SELECT +
"p." + SessionsTable.USER_UUID + "," +
"SUM(p." + SessionsTable.AFK_TIME + ") as afk" +
FROM + SessionsTable.TABLE_NAME + " p" +
INNER_JOIN + '(' + selectActivityIndexSQL() + ") q2 on q2." + SessionsTable.USER_UUID + "=p." + SessionsTable.USER_UUID +
WHERE + "p." + SessionsTable.SESSION_END + "<=?" +
AND + "p." + SessionsTable.SESSION_START + ">=?" +
AND + "q2.activity_index>=?" +
AND + "q2.activity_index<?" +
GROUP_BY + "p." + SessionsTable.USER_UUID;
String selectAverage = SELECT + "AVG(afk) as average" + FROM + '(' + selectPlaytimePerPlayer + ") q1";
return database.query(new QueryStatement<Long>(selectAverage, 100) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
setSelectActivityIndexSQLParameters(statement, 1, threshold, before);
statement.setLong(9, before);
statement.setLong(10, after);
statement.setDouble(11, ActivityIndex.REGULAR);
statement.setDouble(12, 5.1);
}
@Override
public Long processResults(ResultSet set) throws SQLException {
return set.next() ? set.getLong("average") : 0;
}
});
};
}
public static Query<Collection<ActivityIndex>> activityIndexForNewPlayers(long after, long before, Long threshold) {
String selectNewUUIDs = SELECT + UsersTable.USER_UUID +
FROM + UsersTable.TABLE_NAME +
WHERE + UsersTable.REGISTERED + "<=?" +
AND + UsersTable.REGISTERED + ">=?";
String sql = SELECT + "activity_index" +
FROM + '(' + selectNewUUIDs + ") n" +
INNER_JOIN + '(' + selectActivityIndexSQL() + ") a on n." + SessionsTable.USER_UUID + "=a." + SessionsTable.USER_UUID;
return new QueryStatement<Collection<ActivityIndex>>(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setLong(1, before);
statement.setLong(2, after);
setSelectActivityIndexSQLParameters(statement, 3, threshold, before);
}
@Override
public Collection<ActivityIndex> processResults(ResultSet set) throws SQLException {
Collection<ActivityIndex> indexes = new ArrayList<>();
while (set.next()) {
indexes.add(new ActivityIndex(set.getDouble("activity_index"), before));
}
return indexes;
}
};
}
public static Query<ActivityIndex> averageActivityIndexForRetainedPlayers(long after, long before, Long threshold) {
String selectNewUUIDs = SELECT + UsersTable.USER_UUID +
FROM + UsersTable.TABLE_NAME +
WHERE + UsersTable.REGISTERED + "<=?" +
AND + UsersTable.REGISTERED + ">=?";
String selectUniqueUUIDs = SELECT + "DISTINCT " + SessionsTable.USER_UUID +
FROM + SessionsTable.TABLE_NAME +
WHERE + SessionsTable.SESSION_START + ">=?" +
AND + SessionsTable.SESSION_END + "<=?";
String sql = SELECT + "AVG(activity_index) as average" +
FROM + '(' + selectNewUUIDs + ") n" +
INNER_JOIN + '(' + selectUniqueUUIDs + ") u on n." + SessionsTable.USER_UUID + "=u." + SessionsTable.USER_UUID +
INNER_JOIN + '(' + selectActivityIndexSQL() + ") a on n." + SessionsTable.USER_UUID + "=a." + SessionsTable.USER_UUID;
return new QueryStatement<ActivityIndex>(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setLong(1, before);
statement.setLong(2, after);
// Have played in the last half of the time frame
long half = before - (after - before) / 2;
statement.setLong(3, half);
statement.setLong(4, before);
setSelectActivityIndexSQLParameters(statement, 5, threshold, before);
}
@Override
public ActivityIndex processResults(ResultSet set) throws SQLException {
return set.next() ? new ActivityIndex(set.getDouble("average"), before) : new ActivityIndex(0.0, before);
}
};
}
public static Query<ActivityIndex> averageActivityIndexForNonRetainedPlayers(long after, long before, Long threshold) {
String selectNewUUIDs = SELECT + UsersTable.USER_UUID +
FROM + UsersTable.TABLE_NAME +
WHERE + UsersTable.REGISTERED + "<=?" +
AND + UsersTable.REGISTERED + ">=?";
String selectUniqueUUIDs = SELECT + "DISTINCT " + SessionsTable.USER_UUID +
FROM + SessionsTable.TABLE_NAME +
WHERE + SessionsTable.SESSION_START + ">=?" +
AND + SessionsTable.SESSION_END + "<=?";
String sql = SELECT + "AVG(activity_index) as average" +
FROM + '(' + selectNewUUIDs + ") n" +
LEFT_JOIN + '(' + selectUniqueUUIDs + ") u on n." + SessionsTable.USER_UUID + "=u." + SessionsTable.USER_UUID +
INNER_JOIN + '(' + selectActivityIndexSQL() + ") a on n." + SessionsTable.USER_UUID + "=a." + SessionsTable.USER_UUID +
WHERE + "n." + SessionsTable.USER_UUID + IS_NULL;
return new QueryStatement<ActivityIndex>(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setLong(1, before);
statement.setLong(2, after);
// Have played in the last half of the time frame
long half = before - (after - before) / 2;
statement.setLong(3, half);
statement.setLong(4, before);
setSelectActivityIndexSQLParameters(statement, 5, threshold, before);
}
@Override
public ActivityIndex processResults(ResultSet set) throws SQLException {
return set.next() ? new ActivityIndex(set.getDouble("average"), before) : new ActivityIndex(0.0, before);
}
};
}
}

View File

@ -58,6 +58,21 @@ public class PlayerCountQueries {
}; };
} }
private static QueryStatement<Integer> queryPlayerCount(String sql, long after, long before) {
return new QueryStatement<Integer>(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setLong(1, before);
statement.setLong(2, after);
}
@Override
public Integer processResults(ResultSet set) throws SQLException {
return set.next() ? set.getInt("player_count") : 0;
}
};
}
public static Query<Integer> uniquePlayerCount(long after, long before, UUID serverUUID) { public static Query<Integer> uniquePlayerCount(long after, long before, UUID serverUUID) {
String sql = SELECT + "COUNT(DISTINCT " + SessionsTable.USER_UUID + ") as player_count" + String sql = SELECT + "COUNT(DISTINCT " + SessionsTable.USER_UUID + ") as player_count" +
FROM + SessionsTable.TABLE_NAME + FROM + SessionsTable.TABLE_NAME +
@ -68,6 +83,22 @@ public class PlayerCountQueries {
return queryPlayerCount(sql, after, before, serverUUID); return queryPlayerCount(sql, after, before, serverUUID);
} }
/**
* Fetch uniquePlayer count for ALL servers.
*
* @param after After epoch ms
* @param before Before epoch ms
* @return Unique player count (players who played within time frame)
*/
public static Query<Integer> uniquePlayerCount(long after, long before) {
String sql = SELECT + "COUNT(DISTINCT " + SessionsTable.USER_UUID + ") as player_count" +
FROM + SessionsTable.TABLE_NAME +
WHERE + SessionsTable.SESSION_END + "<=?" +
AND + SessionsTable.SESSION_START + ">=?";
return queryPlayerCount(sql, after, before);
}
/** /**
* Fetch a EpochMs - Count map of unique players on a server. * Fetch a EpochMs - Count map of unique players on a server.
* *

View File

@ -523,6 +523,39 @@ public class SessionQueries {
}; };
} }
/**
* Fetch average playtime per ALL players.
*
* @param after After epoch ms
* @param before Before epoch ms
* @return Average ms played / player, calculated with grouped sums from sessions table.
*/
public static Query<Long> averagePlaytimePerPlayer(long after, long before) {
return database -> {
String selectPlaytimePerPlayer = SELECT +
SessionsTable.USER_UUID + "," +
"SUM(" + SessionsTable.SESSION_END + '-' + SessionsTable.SESSION_START + ") as playtime" +
FROM + SessionsTable.TABLE_NAME +
WHERE + SessionsTable.SESSION_END + "<=?" +
AND + SessionsTable.SESSION_START + ">=?" +
GROUP_BY + SessionsTable.USER_UUID;
String selectAverage = SELECT + "AVG(playtime) as average" + FROM + '(' + selectPlaytimePerPlayer + ") q1";
return database.query(new QueryStatement<Long>(selectAverage, 100) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setLong(1, before);
statement.setLong(2, after);
}
@Override
public Long processResults(ResultSet set) throws SQLException {
return set.next() ? set.getLong("average") : 0;
}
});
};
}
public static Query<Long> averageAfkPerPlayer(long after, long before, UUID serverUUID) { public static Query<Long> averageAfkPerPlayer(long after, long before, UUID serverUUID) {
return database -> { return database -> {
String selectAfkPerPlayer = SELECT + String selectAfkPerPlayer = SELECT +
@ -551,6 +584,39 @@ public class SessionQueries {
}; };
} }
/**
* Fetch average Afk per ALL players.
*
* @param after After epoch ms
* @param before Before epoch ms
* @return Average ms afk / player, calculated with grouped sums from sessions table.
*/
public static Query<Long> averageAfkPerPlayer(long after, long before) {
return database -> {
String selectAfkPerPlayer = SELECT +
SessionsTable.USER_UUID + "," +
"SUM(" + SessionsTable.AFK_TIME + ") as afk" +
FROM + SessionsTable.TABLE_NAME +
WHERE + SessionsTable.SESSION_END + "<=?" +
AND + SessionsTable.SESSION_START + ">=?" +
GROUP_BY + SessionsTable.USER_UUID;
String selectAverage = SELECT + "AVG(afk) as average" + FROM + '(' + selectAfkPerPlayer + ") q1";
return database.query(new QueryStatement<Long>(selectAverage, 100) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setLong(1, before);
statement.setLong(2, after);
}
@Override
public Long processResults(ResultSet set) throws SQLException {
return set.next() ? set.getLong("average") : 0;
}
});
};
}
public static Query<Long> afkTime(long after, long before, UUID serverUUID) { public static Query<Long> afkTime(long after, long before, UUID serverUUID) {
String sql = SELECT + "SUM(" + SessionsTable.AFK_TIME + ") as afk_time" + String sql = SELECT + "SUM(" + SessionsTable.AFK_TIME + ") as afk_time" +
FROM + SessionsTable.TABLE_NAME + FROM + SessionsTable.TABLE_NAME +

View File

@ -46,7 +46,7 @@ import java.util.concurrent.TimeUnit;
* @author Rsl1122 * @author Rsl1122
*/ */
@Singleton @Singleton
public class OnlineActivityOverviewJSONParser implements TabJSONParser<Map<String, Object>> { public class OnlineActivityOverviewJSONParser implements ServerTabJSONParser<Map<String, Object>> {
private PlanConfig config; private PlanConfig config;
private DBSystem dbSystem; private DBSystem dbSystem;

View File

@ -40,7 +40,7 @@ import java.util.concurrent.TimeUnit;
* @author Rsl1122 * @author Rsl1122
*/ */
@Singleton @Singleton
public class PerformanceJSONParser implements TabJSONParser<Map<String, Object>> { public class PerformanceJSONParser implements ServerTabJSONParser<Map<String, Object>> {
private final PlanConfig config; private final PlanConfig config;
private final DBSystem dbSystem; private final DBSystem dbSystem;

View File

@ -39,7 +39,7 @@ import java.util.concurrent.TimeUnit;
* @author Rsl1122 * @author Rsl1122
*/ */
@Singleton @Singleton
public class PlayerBaseOverviewJSONParser implements TabJSONParser<Map<String, Object>> { public class PlayerBaseOverviewJSONParser implements ServerTabJSONParser<Map<String, Object>> {
private PlanConfig config; private PlanConfig config;
private DBSystem dbSystem; private DBSystem dbSystem;

View File

@ -33,7 +33,7 @@ import java.util.concurrent.TimeUnit;
* @author Rsl1122 * @author Rsl1122
*/ */
@Singleton @Singleton
public class PvPPvEJSONParser implements TabJSONParser<Map<String, Object>> { public class PvPPvEJSONParser implements ServerTabJSONParser<Map<String, Object>> {
private DBSystem dbSystem; private DBSystem dbSystem;

View File

@ -46,7 +46,7 @@ import java.util.concurrent.TimeUnit;
* @author Rsl1122 * @author Rsl1122
*/ */
@Singleton @Singleton
public class ServerOverviewJSONParser implements TabJSONParser<Map<String, Object>> { public class ServerOverviewJSONParser implements ServerTabJSONParser<Map<String, Object>> {
private final Formatter<Long> day; private final Formatter<Long> day;
private PlanConfig config; private PlanConfig config;

View File

@ -24,7 +24,7 @@ import java.util.function.Function;
* *
* @author Rsl1122 * @author Rsl1122
*/ */
public interface TabJSONParser<T> extends Function<UUID, T> { public interface ServerTabJSONParser<T> extends Function<UUID, T> {
T createJSONAsMap(UUID serverUUID); T createJSONAsMap(UUID serverUUID);

View File

@ -39,7 +39,7 @@ import java.util.concurrent.TimeUnit;
* @author Rsl1122 * @author Rsl1122
*/ */
@Singleton @Singleton
public class SessionsOverviewJSONParser implements TabJSONParser<Map<String, Object>> { public class SessionsOverviewJSONParser implements ServerTabJSONParser<Map<String, Object>> {
private DBSystem dbSystem; private DBSystem dbSystem;

View File

@ -0,0 +1,151 @@
/*
* 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.system.json.network;
import com.djrapitops.plan.db.Database;
import com.djrapitops.plan.db.access.queries.analysis.NetworkActivityIndexQueries;
import com.djrapitops.plan.db.access.queries.analysis.PlayerCountQueries;
import com.djrapitops.plan.db.access.queries.objects.SessionQueries;
import com.djrapitops.plan.system.database.DBSystem;
import com.djrapitops.plan.system.json.Trend;
import com.djrapitops.plan.system.settings.config.PlanConfig;
import com.djrapitops.plan.system.settings.paths.TimeSettings;
import com.djrapitops.plan.utilities.formatting.Formatter;
import com.djrapitops.plan.utilities.formatting.Formatters;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Parses JSON payload for /server-page Playerbase Overview tab.
*
* @author Rsl1122
*/
@Singleton
public class NetworkPlayerBaseOverviewJSONParser implements NetworkTabJSONParser<Map<String, Object>> {
private PlanConfig config;
private DBSystem dbSystem;
private Formatter<Long> timeAmount;
private Formatter<Double> percentage;
@Inject
public NetworkPlayerBaseOverviewJSONParser(
PlanConfig config,
DBSystem dbSystem,
Formatters formatters
) {
this.config = config;
this.dbSystem = dbSystem;
timeAmount = formatters.timeAmount();
percentage = formatters.percentage();
}
public Map<String, Object> createJSONAsMap() {
Map<String, Object> serverOverview = new HashMap<>();
serverOverview.put("trends", createTrendsMap());
serverOverview.put("insights", createInsightsMap());
return serverOverview;
}
private Map<String, Object> createTrendsMap() {
Database db = dbSystem.getDatabase();
long now = System.currentTimeMillis();
long monthAgo = now - TimeUnit.DAYS.toMillis(30L);
long twoMonthsAgo = now - TimeUnit.DAYS.toMillis(60L);
Long playThreshold = config.get(TimeSettings.ACTIVE_PLAY_THRESHOLD);
Map<String, Object> trends = new HashMap<>();
Integer playersBefore = db.query(PlayerCountQueries.uniquePlayerCount(0L, monthAgo));
Integer playersAfter = db.query(PlayerCountQueries.uniquePlayerCount(0L, now));
trends.put("total_players_then", playersBefore);
trends.put("total_players_now", playersAfter);
trends.put("total_players_trend", new Trend(playersBefore, playersAfter, false));
Integer regularBefore = db.query(NetworkActivityIndexQueries.fetchRegularPlayerCount(monthAgo, playThreshold));
Integer regularAfter = db.query(NetworkActivityIndexQueries.fetchRegularPlayerCount(now, playThreshold));
trends.put("regular_players_then", regularBefore);
trends.put("regular_players_now", regularAfter);
trends.put("regular_players_trend", new Trend(regularBefore, regularAfter, false));
Long avgPlaytimeBefore = db.query(SessionQueries.averagePlaytimePerPlayer(twoMonthsAgo, monthAgo));
Long avgPlaytimeAfter = db.query(SessionQueries.averagePlaytimePerPlayer(monthAgo, now));
trends.put("playtime_avg_then", timeAmount.apply(avgPlaytimeBefore));
trends.put("playtime_avg_now", timeAmount.apply(avgPlaytimeAfter));
trends.put("playtime_avg_trend", new Trend(avgPlaytimeBefore, avgPlaytimeAfter, false, timeAmount));
Long avgAfkBefore = db.query(SessionQueries.averageAfkPerPlayer(twoMonthsAgo, monthAgo));
Long avgAfkAfter = db.query(SessionQueries.averageAfkPerPlayer(monthAgo, now));
double afkPercBefore = avgPlaytimeBefore != 0 ? (double) avgAfkBefore / avgPlaytimeBefore : 0;
double afkPercAfter = avgPlaytimeAfter != 0 ? (double) avgAfkAfter / avgPlaytimeAfter : 0;
trends.put("afk_then", percentage.apply(afkPercBefore));
trends.put("afk_now", percentage.apply(afkPercAfter));
trends.put("afk_trend", new Trend(afkPercBefore, afkPercAfter, Trend.REVERSED, percentage));
Long avgRegularPlaytimeBefore = db.query(NetworkActivityIndexQueries.averagePlaytimePerRegularPlayer(twoMonthsAgo, monthAgo, playThreshold));
Long avgRegularPlaytimeAfter = db.query(NetworkActivityIndexQueries.averagePlaytimePerRegularPlayer(monthAgo, now, playThreshold));
trends.put("regular_playtime_avg_then", timeAmount.apply(avgRegularPlaytimeBefore));
trends.put("regular_playtime_avg_now", timeAmount.apply(avgRegularPlaytimeAfter));
trends.put("regular_playtime_avg_trend", new Trend(avgRegularPlaytimeBefore, avgRegularPlaytimeAfter, false, timeAmount));
Long avgRegularSessionLengthBefore = db.query(NetworkActivityIndexQueries.averageSessionLengthPerRegularPlayer(twoMonthsAgo, monthAgo, playThreshold));
Long avgRegularSessionLengthAfter = db.query(NetworkActivityIndexQueries.averageSessionLengthPerRegularPlayer(monthAgo, now, playThreshold));
trends.put("regular_session_avg_then", timeAmount.apply(avgRegularSessionLengthBefore));
trends.put("regular_session_avg_now", timeAmount.apply(avgRegularSessionLengthAfter));
trends.put("regular_session_avg_trend", new Trend(avgRegularSessionLengthBefore, avgRegularSessionLengthAfter, false, timeAmount));
Long avgRegularAfkBefore = db.query(NetworkActivityIndexQueries.averageAFKPerRegularPlayer(twoMonthsAgo, monthAgo, playThreshold));
Long avgRegularAfkAfter = db.query(NetworkActivityIndexQueries.averageAFKPerRegularPlayer(monthAgo, now, playThreshold));
double afkRegularPercBefore = avgRegularPlaytimeBefore != 0 ? (double) avgRegularAfkBefore / avgRegularPlaytimeBefore : 0;
double afkRegularPercAfter = avgRegularPlaytimeAfter != 0 ? (double) avgRegularAfkAfter / avgRegularPlaytimeAfter : 0;
trends.put("regular_afk_avg_then", percentage.apply(afkRegularPercBefore));
trends.put("regular_afk_avg_now", percentage.apply(afkRegularPercAfter));
trends.put("regular_afk_avg_trend", new Trend(afkRegularPercBefore, afkRegularPercAfter, Trend.REVERSED, percentage));
return trends;
}
private Map<String, Object> createInsightsMap() {
Database db = dbSystem.getDatabase();
long now = System.currentTimeMillis();
long halfMonthAgo = now - TimeUnit.DAYS.toMillis(30L);
long monthAgo = now - TimeUnit.DAYS.toMillis(30L);
Long playThreshold = config.get(TimeSettings.ACTIVE_PLAY_THRESHOLD);
Map<String, Object> insights = new HashMap<>();
int newToRegular = db.query(NetworkActivityIndexQueries.countNewPlayersTurnedRegular(monthAgo, now, playThreshold));
Integer newToRegularBefore = db.query(NetworkActivityIndexQueries.countNewPlayersTurnedRegular(monthAgo, halfMonthAgo, playThreshold));
Integer newToRegularAfter = db.query(NetworkActivityIndexQueries.countNewPlayersTurnedRegular(halfMonthAgo, now, playThreshold));
insights.put("new_to_regular", newToRegular);
insights.put("new_to_regular_trend", new Trend(newToRegularBefore, newToRegularAfter, false));
Integer regularToInactive = db.query(NetworkActivityIndexQueries.countRegularPlayersTurnedInactive(monthAgo, now, playThreshold));
Integer regularToInactiveBefore = db.query(NetworkActivityIndexQueries.countRegularPlayersTurnedInactive(monthAgo, halfMonthAgo, playThreshold));
Integer regularToInactiveAfter = db.query(NetworkActivityIndexQueries.countRegularPlayersTurnedInactive(halfMonthAgo, now, playThreshold));
insights.put("regular_to_inactive", regularToInactive);
insights.put("regular_to_inactive_trend", new Trend(regularToInactiveBefore, regularToInactiveAfter, Trend.REVERSED));
return insights;
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.system.json.network;
import java.util.function.Supplier;
/**
* Interface for different tab JSON parsers.
*
* @author Rsl1122
*/
public interface NetworkTabJSONParser<T> extends Supplier<T> {
T createJSONAsMap();
@Override
default T get() {
return createJSONAsMap();
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.system.webserver.pages.json;
import com.djrapitops.plan.api.exceptions.WebUserAuthException;
import com.djrapitops.plan.api.exceptions.connection.WebException;
import com.djrapitops.plan.system.json.network.NetworkTabJSONParser;
import com.djrapitops.plan.system.webserver.Request;
import com.djrapitops.plan.system.webserver.RequestTarget;
import com.djrapitops.plan.system.webserver.auth.Authentication;
import com.djrapitops.plan.system.webserver.pages.PageHandler;
import com.djrapitops.plan.system.webserver.response.Response;
import com.djrapitops.plan.system.webserver.response.data.JSONResponse;
import java.util.function.Supplier;
/**
* Generic Tab JSON handler for any tab's data.
*
* @author Rsl1122
*/
public class NetworkTabJSONHandler<T> implements PageHandler {
private final Supplier<T> jsonParser;
public NetworkTabJSONHandler(NetworkTabJSONParser<T> jsonParser) {
this.jsonParser = jsonParser;
}
@Override
public Response getResponse(Request request, RequestTarget target) throws WebException {
return new JSONResponse(jsonParser.get());
}
@Override
public boolean isAuthorized(Authentication auth, RequestTarget target) throws WebUserAuthException {
return auth.getWebUser().getPermLevel() <= 0;
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.system.webserver.pages.json;
import com.djrapitops.plan.api.exceptions.WebUserAuthException;
import com.djrapitops.plan.system.json.network.NetworkPlayerBaseOverviewJSONParser;
import com.djrapitops.plan.system.json.network.NetworkTabJSONParser;
import com.djrapitops.plan.system.webserver.RequestTarget;
import com.djrapitops.plan.system.webserver.auth.Authentication;
import com.djrapitops.plan.system.webserver.pages.TreePageHandler;
import com.djrapitops.plan.system.webserver.response.ResponseFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Root handler for different JSON end points.
*
* @author Rsl1122
*/
@Singleton
public class NeworkJSONHandler extends TreePageHandler {
@Inject
public NeworkJSONHandler(
ResponseFactory responseFactory,
NetworkPlayerBaseOverviewJSONParser playerBaseOverviewJSONParser
) {
super(responseFactory);
registerPage("playerbaseOverview", playerBaseOverviewJSONParser);
}
private <T> void registerPage(String identifier, NetworkTabJSONParser<T> tabJSONParser) {
registerPage(identifier, new NetworkTabJSONHandler<>(tabJSONParser));
}
@Override
public boolean isAuthorized(Authentication auth, RequestTarget target) throws WebUserAuthException {
return true;
}
}

View File

@ -41,7 +41,6 @@ public class RootJSONHandler extends TreePageHandler {
public RootJSONHandler( public RootJSONHandler(
ResponseFactory responseFactory, ResponseFactory responseFactory,
Identifiers identifiers, Identifiers identifiers,
JSONFactory jsonFactory,
GraphsJSONHandler graphsJSONHandler, GraphsJSONHandler graphsJSONHandler,
SessionsJSONHandler sessionsJSONHandler, SessionsJSONHandler sessionsJSONHandler,
PlayersTableJSONHandler playersTableJSONHandler, PlayersTableJSONHandler playersTableJSONHandler,
@ -53,7 +52,8 @@ public class RootJSONHandler extends TreePageHandler {
PvPPvEJSONParser pvPPvEJSONParser, PvPPvEJSONParser pvPPvEJSONParser,
PlayerBaseOverviewJSONParser playerBaseOverviewJSONParser, PlayerBaseOverviewJSONParser playerBaseOverviewJSONParser,
PerformanceJSONParser performanceJSONParser, PerformanceJSONParser performanceJSONParser,
PlayerJSONHandler playerJSONHandler PlayerJSONHandler playerJSONHandler,
NeworkJSONHandler neworkJSONHandler
) { ) {
super(responseFactory); super(responseFactory);
@ -73,9 +73,10 @@ public class RootJSONHandler extends TreePageHandler {
registerPage("performanceOverview", performanceJSONParser); registerPage("performanceOverview", performanceJSONParser);
registerPage("player", playerJSONHandler); registerPage("player", playerJSONHandler);
registerPage("network", neworkJSONHandler);
} }
private <T> void registerPage(String identifier, TabJSONParser<T> tabJSONParser) { private <T> void registerPage(String identifier, ServerTabJSONParser<T> tabJSONParser) {
registerPage(identifier, new ServerTabJSONHandler<>(identifiers, tabJSONParser)); registerPage(identifier, new ServerTabJSONHandler<>(identifiers, tabJSONParser));
} }

View File

@ -19,7 +19,7 @@ package com.djrapitops.plan.system.webserver.pages.json;
import com.djrapitops.plan.api.exceptions.WebUserAuthException; import com.djrapitops.plan.api.exceptions.WebUserAuthException;
import com.djrapitops.plan.api.exceptions.connection.WebException; import com.djrapitops.plan.api.exceptions.connection.WebException;
import com.djrapitops.plan.system.Identifiers; import com.djrapitops.plan.system.Identifiers;
import com.djrapitops.plan.system.json.TabJSONParser; import com.djrapitops.plan.system.json.ServerTabJSONParser;
import com.djrapitops.plan.system.webserver.Request; import com.djrapitops.plan.system.webserver.Request;
import com.djrapitops.plan.system.webserver.RequestTarget; import com.djrapitops.plan.system.webserver.RequestTarget;
import com.djrapitops.plan.system.webserver.auth.Authentication; import com.djrapitops.plan.system.webserver.auth.Authentication;
@ -40,7 +40,7 @@ public class ServerTabJSONHandler<T> implements PageHandler {
private final Identifiers identifiers; private final Identifiers identifiers;
private final Function<UUID, T> jsonParser; private final Function<UUID, T> jsonParser;
public ServerTabJSONHandler(Identifiers identifiers, TabJSONParser<T> jsonParser) { public ServerTabJSONHandler(Identifiers identifiers, ServerTabJSONParser<T> jsonParser) {
this.identifiers = identifiers; this.identifiers = identifiers;
this.jsonParser = jsonParser; this.jsonParser = jsonParser;
} }

View File

@ -17,18 +17,20 @@
package com.djrapitops.plan.utilities.html.pages; package com.djrapitops.plan.utilities.html.pages;
import com.djrapitops.plan.api.exceptions.ParseException; import com.djrapitops.plan.api.exceptions.ParseException;
import com.djrapitops.plan.data.store.containers.NetworkContainer; import com.djrapitops.plan.extension.implementation.results.server.ExtensionServerData;
import com.djrapitops.plan.data.store.keys.NetworkKeys; import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionServerDataQuery;
import com.djrapitops.plan.data.store.keys.ServerKeys; import com.djrapitops.plan.system.database.DBSystem;
import com.djrapitops.plan.system.file.PlanFiles; import com.djrapitops.plan.system.file.PlanFiles;
import com.djrapitops.plan.system.info.server.properties.ServerProperties; import com.djrapitops.plan.system.info.server.ServerInfo;
import com.djrapitops.plan.system.settings.config.PlanConfig;
import com.djrapitops.plan.system.settings.paths.ProxySettings;
import com.djrapitops.plan.system.settings.theme.Theme;
import com.djrapitops.plan.system.settings.theme.ThemeVal;
import com.djrapitops.plan.system.update.VersionCheckSystem; import com.djrapitops.plan.system.update.VersionCheckSystem;
import com.djrapitops.plan.utilities.formatting.Formatters; import com.djrapitops.plan.utilities.formatting.Formatters;
import com.djrapitops.plan.utilities.formatting.PlaceholderReplacer; import com.djrapitops.plan.utilities.formatting.PlaceholderReplacer;
import java.util.ArrayList; import java.util.List;
import static com.djrapitops.plan.data.store.keys.NetworkKeys.*;
/** /**
* Html String parser for /network page. * Html String parser for /network page.
@ -37,57 +39,57 @@ import static com.djrapitops.plan.data.store.keys.NetworkKeys.*;
*/ */
public class NetworkPage implements Page { public class NetworkPage implements Page {
private final NetworkContainer networkContainer; private final DBSystem dbSystem;
private final VersionCheckSystem versionCheckSystem; private final VersionCheckSystem versionCheckSystem;
private final PlanFiles files; private final PlanFiles files;
private final ServerProperties serverProperties; private final PlanConfig config;
private final Theme theme;
private final ServerInfo serverInfo;
private final Formatters formatters; private final Formatters formatters;
NetworkPage( NetworkPage(
NetworkContainer networkContainer, DBSystem dbSystem,
VersionCheckSystem versionCheckSystem, VersionCheckSystem versionCheckSystem,
PlanFiles files, PlanFiles files,
ServerProperties serverProperties, PlanConfig config,
Theme theme,
ServerInfo serverInfo,
Formatters formatters Formatters formatters
) { ) {
this.networkContainer = networkContainer; this.dbSystem = dbSystem;
this.versionCheckSystem = versionCheckSystem; this.versionCheckSystem = versionCheckSystem;
this.files = files; this.files = files;
this.serverProperties = serverProperties; this.config = config;
this.theme = theme;
this.serverInfo = serverInfo;
this.formatters = formatters; this.formatters = formatters;
} }
@Override @Override
public String toHtml() throws ParseException { public String toHtml() throws ParseException {
try { try {
networkContainer.putSupplier(NetworkKeys.PLAYERS_ONLINE, serverProperties::getOnlinePlayers); PlaceholderReplacer placeholders = new PlaceholderReplacer();
PlaceholderReplacer placeholderReplacer = new PlaceholderReplacer(); placeholders.put("networkDisplayName", config.get(ProxySettings.NETWORK_NAME));
placeholderReplacer.addAllPlaceholdersFrom(networkContainer,
VERSION, NETWORK_NAME, TIME_ZONE,
PLAYERS_ONLINE, PLAYERS_ONLINE_SERIES, PLAYERS_TOTAL, PLAYERS_GRAPH_COLOR,
REFRESH_TIME_F, RECENT_PEAK_TIME_F, ALL_TIME_PEAK_TIME_F,
PLAYERS_ALL_TIME_PEAK, PLAYERS_RECENT_PEAK,
PLAYERS_DAY, PLAYERS_WEEK, PLAYERS_MONTH,
PLAYERS_NEW_DAY, PLAYERS_NEW_WEEK, PLAYERS_NEW_MONTH,
WORLD_MAP_SERIES, WORLD_MAP_HIGH_COLOR, WORLD_MAP_LOW_COLOR,
COUNTRY_CATEGORIES, COUNTRY_SERIES,
HEALTH_INDEX, HEALTH_NOTES,
ACTIVITY_PIE_SERIES, ACTIVITY_STACK_SERIES, ACTIVITY_STACK_CATEGORIES,
SERVERS_TAB
);
placeholderReplacer.put("update", versionCheckSystem.getUpdateHtml().orElse(""));
ServerPluginTabs serverPluginTabs = new ServerPluginTabs(networkContainer.getBungeeContainer().getValue(ServerKeys.EXTENSION_DATA).orElse(new ArrayList<>()), formatters); placeholders.put("gmPieColors", theme.getValue(ThemeVal.GRAPH_GM_PIE));
placeholders.put("playersGraphColor", theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE));
placeholders.put("timeZone", config.getTimeZoneOffsetHours());
String nav = serverPluginTabs.getNav(); placeholders.put("update", versionCheckSystem.getUpdateHtml().orElse(""));
String tabs = serverPluginTabs.getTabs();
placeholderReplacer.put("navPluginsTabs", nav); List<ExtensionServerData> extensionData = dbSystem.getDatabase()
placeholderReplacer.put("tabsPlugins", tabs); .query(new ExtensionServerDataQuery(serverInfo.getServerUUID()));
ServerPluginTabs pluginTabs = new ServerPluginTabs(extensionData, formatters);
return placeholderReplacer.apply(files.getCustomizableResourceOrDefault("web/network.html").asString()); String nav = pluginTabs.getNav();
String tabs = pluginTabs.getTabs();
placeholders.put("navPluginsTabs", nav);
placeholders.put("tabsPlugins", tabs);
return placeholders.apply(files.getCustomizableResourceOrDefault("web/network.html").asString());
} catch (Exception e) { } catch (Exception e) {
throw new ParseException(e); throw new ParseException(e);
} }

View File

@ -110,6 +110,7 @@ public class PageFactory {
theme.get(), theme.get(),
versionCheckSystem.get(), versionCheckSystem.get(),
fileSystem.get(), fileSystem.get(),
dbSystem.get(),
formatters.get() formatters.get()
)).orElseThrow(() -> new NotFoundException("Server not found in the database")); )).orElseThrow(() -> new NotFoundException("Server not found in the database"));
} }
@ -161,7 +162,7 @@ public class PageFactory {
public NetworkPage networkPage() { public NetworkPage networkPage() {
NetworkContainer networkContainer = dbSystem.get().getDatabase() NetworkContainer networkContainer = dbSystem.get().getDatabase()
.query(ContainerFetchQueries.fetchNetworkContainer()); // Not cached, big. .query(ContainerFetchQueries.fetchNetworkContainer()); // Not cached, big.
return new NetworkPage(networkContainer, return new NetworkPage(dbSystem.get(),
versionCheckSystem.get(), fileSystem.get(), serverInfo.get().getServerProperties(), formatters.get()); versionCheckSystem.get(), fileSystem.get(), config.get(), theme.get(), serverInfo.get(), formatters.get());
} }
} }

View File

@ -20,6 +20,9 @@ import com.djrapitops.plan.api.exceptions.ParseException;
import com.djrapitops.plan.data.store.containers.DataContainer; import com.djrapitops.plan.data.store.containers.DataContainer;
import com.djrapitops.plan.data.store.containers.RawDataContainer; import com.djrapitops.plan.data.store.containers.RawDataContainer;
import com.djrapitops.plan.data.store.keys.AnalysisKeys; import com.djrapitops.plan.data.store.keys.AnalysisKeys;
import com.djrapitops.plan.extension.implementation.results.server.ExtensionServerData;
import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionServerDataQuery;
import com.djrapitops.plan.system.database.DBSystem;
import com.djrapitops.plan.system.file.PlanFiles; import com.djrapitops.plan.system.file.PlanFiles;
import com.djrapitops.plan.system.info.server.Server; import com.djrapitops.plan.system.info.server.Server;
import com.djrapitops.plan.system.settings.config.PlanConfig; import com.djrapitops.plan.system.settings.config.PlanConfig;
@ -31,6 +34,7 @@ import com.djrapitops.plan.utilities.formatting.Formatters;
import com.djrapitops.plan.utilities.formatting.PlaceholderReplacer; import com.djrapitops.plan.utilities.formatting.PlaceholderReplacer;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import static com.djrapitops.plan.data.store.keys.AnalysisKeys.*; import static com.djrapitops.plan.data.store.keys.AnalysisKeys.*;
@ -46,6 +50,7 @@ public class ServerPage implements Page {
private Theme theme; private Theme theme;
private final VersionCheckSystem versionCheckSystem; private final VersionCheckSystem versionCheckSystem;
private final PlanFiles files; private final PlanFiles files;
private final DBSystem dbSystem;
private Formatters formatters; private Formatters formatters;
ServerPage( ServerPage(
@ -54,6 +59,7 @@ public class ServerPage implements Page {
Theme theme, Theme theme,
VersionCheckSystem versionCheckSystem, VersionCheckSystem versionCheckSystem,
PlanFiles files, PlanFiles files,
DBSystem dbSystem,
Formatters formatters Formatters formatters
) { ) {
this.server = server; this.server = server;
@ -61,20 +67,18 @@ public class ServerPage implements Page {
this.theme = theme; this.theme = theme;
this.versionCheckSystem = versionCheckSystem; this.versionCheckSystem = versionCheckSystem;
this.files = files; this.files = files;
this.dbSystem = dbSystem;
this.formatters = formatters; this.formatters = formatters;
} }
@Override @Override
public String toHtml() throws ParseException { public String toHtml() throws ParseException {
PlaceholderReplacer placeholderReplacer = new PlaceholderReplacer(); PlaceholderReplacer placeholders = new PlaceholderReplacer();
placeholderReplacer.put("serverName", server.getIdentifiableName()); placeholders.put("serverName", server.getIdentifiableName());
placeholderReplacer.put("serverDisplayName", server.getName()); placeholders.put("serverDisplayName", server.getName());
long now = System.currentTimeMillis();
DataContainer constants = new RawDataContainer(); DataContainer constants = new RawDataContainer();
constants.putRawData(AnalysisKeys.REFRESH_TIME_F, formatters.clockLong().apply(now));
constants.putRawData(AnalysisKeys.REFRESH_TIME_FULL_F, formatters.secondLong().apply(now));
constants.putRawData(AnalysisKeys.VERSION, versionCheckSystem.getCurrentVersion()); constants.putRawData(AnalysisKeys.VERSION, versionCheckSystem.getCurrentVersion());
constants.putRawData(AnalysisKeys.TIME_ZONE, config.getTimeZoneOffsetHours()); constants.putRawData(AnalysisKeys.TIME_ZONE, config.getTimeZoneOffsetHours());
@ -97,7 +101,7 @@ public class ServerPage implements Page {
constants.putRawData(AnalysisKeys.MAX_PING_COLOR, theme.getValue(ThemeVal.GRAPH_MAX_PING)); constants.putRawData(AnalysisKeys.MAX_PING_COLOR, theme.getValue(ThemeVal.GRAPH_MAX_PING));
constants.putRawData(AnalysisKeys.MIN_PING_COLOR, theme.getValue(ThemeVal.GRAPH_MIN_PING)); constants.putRawData(AnalysisKeys.MIN_PING_COLOR, theme.getValue(ThemeVal.GRAPH_MIN_PING));
placeholderReplacer.addAllPlaceholdersFrom(constants, placeholders.addAllPlaceholdersFrom(constants,
VERSION, TIME_ZONE, VERSION, TIME_ZONE,
FIRST_DAY, TPS_MEDIUM, TPS_HIGH, FIRST_DAY, TPS_MEDIUM, TPS_HIGH,
DISK_MEDIUM, DISK_HIGH, DISK_MEDIUM, DISK_HIGH,
@ -110,14 +114,24 @@ public class ServerPage implements Page {
); );
if (server.isProxy()) { if (server.isProxy()) {
placeholderReplacer.put("backButton", "<li><a title=\"to Network page\" href=\"/network\"><i class=\"material-icons\">arrow_back</i><i class=\"material-icons\">cloud</i></a></li>"); placeholders.put("backButton", "<li><a title=\"to Network page\" href=\"/network\"><i class=\"material-icons\">arrow_back</i><i class=\"material-icons\">cloud</i></a></li>");
} else { } else {
placeholderReplacer.put("backButton", ""); placeholders.put("backButton", "");
} }
placeholderReplacer.put("update", versionCheckSystem.getUpdateHtml().orElse("")); placeholders.put("update", versionCheckSystem.getUpdateHtml().orElse(""));
List<ExtensionServerData> extensionData = dbSystem.getDatabase()
.query(new ExtensionServerDataQuery(server.getUuid()));
ServerPluginTabs pluginTabs = new ServerPluginTabs(extensionData, formatters);
String nav = pluginTabs.getNav();
String tabs = pluginTabs.getTabs();
placeholders.put("navPluginsTabs", nav);
placeholders.put("tabsPlugins", tabs);
try { try {
return placeholderReplacer.apply(files.getCustomizableResourceOrDefault("web/server.html").asString()); return placeholders.apply(files.getCustomizableResourceOrDefault("web/server.html").asString());
} catch (IOException e) { } catch (IOException e) {
throw new ParseException(e); throw new ParseException(e);
} }

View File

@ -269,9 +269,9 @@ function loadPlayerbaseOverviewValues(json, error) {
$(element).find('#data_regular_session_avg_then').text(data.regular_session_avg_then); $(element).find('#data_regular_session_avg_then').text(data.regular_session_avg_then);
$(element).find('#data_regular_session_avg_now').text(data.regular_session_avg_now); $(element).find('#data_regular_session_avg_now').text(data.regular_session_avg_now);
$(element).find('#data_regular_session_avg_trend').replaceWith(trend(data.regular_session_avg_trend)); $(element).find('#data_regular_session_avg_trend').replaceWith(trend(data.regular_session_avg_trend));
$(element).find('#data_regular_afk_then').text(data.regular_afk_then); $(element).find('#data_regular_afk_then').text(data.regular_afk_avg_then);
$(element).find('#data_regular_afk_now').text(data.regular_afk_now); $(element).find('#data_regular_afk_now').text(data.regular_afk_avg_now);
$(element).find('#data_regular_afk_trend').replaceWith(trend(data.regular_afk_trend)); $(element).find('#data_regular_afk_trend').replaceWith(trend(data.regular_afk_avg_trend));
// Insights // Insights
data = json.insights; data = json.insights;

View File

@ -25,7 +25,9 @@
</head> </head>
<body id="page-top"> <body id="page-top">
<script>
var gmPieColors = [${gmPieColors}];
</script>
<div class="page-loader"> <div class="page-loader">
<span class="loader"></span> <span class="loader"></span>
<p class="loader-text">Please wait..</span> <p class="loader-text">Please wait..</span>
@ -796,16 +798,16 @@
</div> <!-- /.container-fluid --> </div> <!-- /.container-fluid -->
</div> <!-- End of Sessions tab --> </div> <!-- End of Sessions tab -->
<!-- Begin Playerbase Overview Tab --> <!-- Begin Playerbase Overview Tab -->
<div class="tab"> <div class="tab" id="playerbase-overview">
<div class="container-fluid mt-4"> <div class="container-fluid mt-4">
<!-- Page Heading --> <!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4"> <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>Network Name <h1 class="h3 mb-0 text-gray-800"><i class="sidebar-toggler fa fa-fw fa-bars"></i>${networkDisplayName}
&middot; Playerbase Overview</h1> &middot; Playerbase Overview</h1>
</div> </div>
<div class="row"> <div class="row">
<!-- New & Unique Players Chart --> <!-- Playerbase Chart -->
<div class="col-xl-8 col-lg-8 col-sm-12"> <div class="col-xl-8 col-lg-8 col-sm-12">
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div <div
@ -814,13 +816,11 @@
class="fas fa-fw fa-chart-line col-amber"></i> class="fas fa-fw fa-chart-line col-amber"></i>
Playerbase development</h6> Playerbase development</h6>
</div> </div>
<div class="chart-area"> <div class="chart-area" id="activityStackGraph"></div>
<canvas id="myAreaChart2"></canvas>
</div>
</div> </div>
</div> </div>
<!-- Calendar --> <!-- Current Playerbase -->
<div class="col-xl-4 col-lg-4 col-sm-12"> <div class="col-xl-4 col-lg-4 col-sm-12">
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div <div
@ -829,15 +829,13 @@
class="fa fa-fw fa-users col-amber"></i> class="fa fa-fw fa-users col-amber"></i>
Current Playerbase</h6> Current Playerbase</h6>
</div> </div>
<div class="chart-area"> <div class="chart-area" id="activityPie"></div>
<canvas id="myAreaChart2"></canvas>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<!-- Online Activity as Numbers --> <!-- Trends for 30 days -->
<div class="col-lg-8 mb-8 col-sm-12"> <div class="col-lg-8 mb-8 col-sm-12">
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
@ -845,71 +843,78 @@
class="fa fa-fw fa-exchange-alt col-amber"></i> class="fa fa-fw fa-exchange-alt col-amber"></i>
Trends for 30 days</h6> Trends for 30 days</h6>
</div> </div>
<table class="table"> <table class="table" id="data_trends">
<thead> <thead>
<th><i class="text-success fa fa-caret-up"></i><i <tr>
class="text-danger fa fa-caret-down"></i> <th><i class="text-success fa fa-caret-up"></i><i
<small>Comparing 15 days</small> class="text-danger fa fa-caret-down"></i>
</th> <small>Comparing 30d ago to Now</small>
<th>30 days ago</th> </th>
<th>Now</th> <th>30 days ago</th>
<th>Trend</th> <th>Now</th>
<th>Trend</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><i class="fa fa-fw fa-users col-black"></i> Total Players</td> <td><i class="fa fa-fw fa-users col-black"></i> Total Players</td>
<td>4532</td> <td id="data_total_players_then"></td>
<td>5043</td> <td id="data_total_players_now"></td>
<td><span class="badge badge-success"><i class="fa fa-caret-up"></i> <td><span id="data_total_players_trend"></span></td>
511</span></td>
</tr> </tr>
<tr> <tr>
<td><i class="fa fa-fw fa-users col-amber"></i> Regular Players</td> <td><i class="fa fa-fw fa-users col-amber"></i> Regular Players</td>
<td>467</td> <td id="data_regular_players_then"></td>
<td>483</td> <td id="data_regular_players_now"></td>
<td><span class="badge badge-success"><i class="fa fa-caret-up"></i> <td><span id="data_regular_players_trend"></span></td>
16</span></td>
</tr> </tr>
<tr> <tr>
<td><i class="col-teal far fa-fw fa-clock"></i> Average Playtime / Player</td> <td><i class="col-green far fa-fw fa-clock"></i> Average Playtime /
<td>8h 5m 56s</td>
<td>8h 53m 24s</td>
<td><span class="badge badge-success"><i class="fa fa-caret-up"></i> 47m
32s</span></td>
</tr>
<tr>
<td><i class="col-teal far fa-fw fa-clock"></i> Average Playtime / Regular
Player Player
</td> </td>
<td>8h 5m 56s</td> <td id="data_playtime_avg_then"></td>
<td>8h 53m 24s</td> <td id="data_playtime_avg_now"></td>
<td><span class="badge badge-success"><i class="fa fa-caret-up"></i> 47m <td><span id="data_playtime_avg_trend"></span></td>
32s</span></td> </tr>
<tr>
<td><i class="col-grey far fa-fw fa-clock"></i> AFK /
Player
</td>
<td id="data_afk_then"></td>
<td id="data_afk_now"></td>
<td><span id="data_afk_trend"></span></td>
</tr>
<tr>
<td><i class="col-green far fa-fw fa-clock"></i> Average Playtime /
Regular
Player
</td>
<td id="data_regular_playtime_avg_then"></td>
<td id="data_regular_playtime_avg_now"></td>
<td><span id="data_regular_playtime_avg_trend"></span></td>
</tr> </tr>
<tr> <tr>
<td><i class="col-teal far fa-fw fa-clock"></i> Average Session Length / <td><i class="col-teal far fa-fw fa-clock"></i> Average Session Length /
Regular Regular
Player Player
</td> </td>
<td>8h 5m 56s</td> <td id="data_regular_session_avg_then"></td>
<td>8h 53m 24s</td> <td id="data_regular_session_avg_now"></td>
<td><span class="badge badge-success"><i class="fa fa-caret-up"></i> 47m <td><span id="data_regular_session_avg_trend"></span></td>
32s</span></td>
</tr> </tr>
<tr> <tr>
<td><i class="col-grey far fa-fw fa-clock"></i> AFK / <td><i class="col-grey far fa-fw fa-clock"></i> AFK /
Regular Regular
Player Player
</td> </td>
<td>34.5%</td> <td id="data_regular_afk_then"></td>
<td>42.4%</td> <td id="data_regular_afk_now"></td>
<td><span class="badge badge-danger"><i class="fa fa-caret-up"></i> <td><span id="data_regular_afk_trend"></span></td>
7.9%</span></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> <!-- End of Online Activity as numbers--> </div> <!-- Trends for 30 days-->
<!-- Insights --> <!-- Insights -->
<div class="col-lg-4 mb-4 col-sm-12"> <div class="col-lg-4 mb-4 col-sm-12">
<div class="card shadow mb-4"> <div class="card shadow mb-4">
@ -918,17 +923,17 @@
class="far fa-fw fa-life-ring col-red"></i> class="far fa-fw fa-life-ring col-red"></i>
Insights for 30 Days</h6> Insights for 30 Days</h6>
</div> </div>
<div class="card-body"> <div class="card-body" id="data_insights">
<p><i class="fa fa-fw fa-user col-light-green"></i> New <i <p><i class="fa fa-fw fa-user col-light-green"></i> New <i
class="fa fa-fw fa-long-arrow-alt-right"></i> <i class="fa fa-fw fa-long-arrow-alt-right"></i> <i
class="fa fa-fw fa-user col-amber"></i> Regular<span class="fa fa-fw fa-user col-amber"></i> Regular<span
class="float-right"><b>32</b> <i class="float-right"><b><span
class="text-success fa fa-caret-up"></i></span></p> id="data_new_to_regular"></span></b></span></p>
<p><i class="fa fa-fw fa-user col-amber"></i> Regular <i <p><i class="fa fa-fw fa-user col-amber"></i> Regular <i
class="fa fa-fw fa-long-arrow-alt-right"></i> <i class="fa fa-fw fa-long-arrow-alt-right"></i> <i
class="fa fa-fw fa-user col-blue-grey"></i> Inactive<span class="fa fa-fw fa-user col-blue-grey"></i> Inactive<span
class="float-right"><b>45</b> <i class="float-right"><b><span
class="text-danger fa fa-caret-up"></i></span></p> id="data_regular_to_inactive"></span></b></span></p>
<p class="float-right"><i class="text-success fa fa-caret-up"></i><i <p class="float-right"><i class="text-success fa fa-caret-up"></i><i
class="text-danger fa fa-caret-down"></i> class="text-danger fa fa-caret-down"></i>
<small>Comparing 30d ago to Now</small> <small>Comparing 30d ago to Now</small>
@ -1265,10 +1270,23 @@
<script src="js/color-selector.js"></script> <script src="js/color-selector.js"></script>
<!-- Page level plugins --> <!-- Page level plugins -->
<script src="vendor/datatables/jquery.dataTables.min.js"></script>
<script src="vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="vendor/highcharts/highstock.js"></script>
<script src="vendor/highcharts/map.js"></script>
<script src="vendor/highcharts/world.js"></script>
<script src="vendor/highcharts/drilldown.js"></script>
<script src="vendor/highcharts/highcharts-more.js"></script>
<script src="vendor/highcharts/no-data-to-display.js"></script>
<!-- Page level custom scripts --> <!-- Page level custom scripts -->
<script src="js/network-values.js"></script>
<script src="js/graphs.js"></script>
<script src="js/sessionAccordion.js"></script>
<script> <script>
setLoadingText('Calculating values..');
jsonRequest("../v1/network/playerbaseOverview", loadPlayerbaseOverviewValues);
setLoadingText('Rendering graphs..'); setLoadingText('Rendering graphs..');
setLoadingText('Sorting players table..'); setLoadingText('Sorting players table..');
setLoadingText('Almost done..'); setLoadingText('Almost done..');

View File

@ -115,7 +115,7 @@
Plugins Plugins
</div> </div>
<li class="plugin-nav" style="display: none;"></li> ${navPluginsTabs}
<!-- Divider --> <!-- Divider -->
<hr class="sidebar-divider"> <hr class="sidebar-divider">
@ -723,7 +723,7 @@
</div> </div>
<div class="row"> <div class="row">
<!-- New & Unique Players Chart --> <!-- Playerbase Chart -->
<div class="col-xl-8 col-lg-8 col-sm-12"> <div class="col-xl-8 col-lg-8 col-sm-12">
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div <div
@ -736,7 +736,7 @@
</div> </div>
</div> </div>
<!-- Calendar --> <!-- Current Playerbase -->
<div class="col-xl-4 col-lg-4 col-sm-12"> <div class="col-xl-4 col-lg-4 col-sm-12">
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div <div
@ -751,7 +751,7 @@
</div> </div>
<div class="row"> <div class="row">
<!-- Online Activity as Numbers --> <!-- Trends for 30 days -->
<div class="col-lg-8 mb-8 col-sm-12"> <div class="col-lg-8 mb-8 col-sm-12">
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
@ -761,13 +761,15 @@
</div> </div>
<table class="table" id="data_trends"> <table class="table" id="data_trends">
<thead> <thead>
<th><i class="text-success fa fa-caret-up"></i><i <tr>
class="text-danger fa fa-caret-down"></i> <th><i class="text-success fa fa-caret-up"></i><i
<small>Comparing 30d ago to Now</small> class="text-danger fa fa-caret-down"></i>
</th> <small>Comparing 30d ago to Now</small>
<th>30 days ago</th> </th>
<th>Now</th> <th>30 days ago</th>
<th>Trend</th> <th>Now</th>
<th>Trend</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
@ -828,7 +830,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> <!-- End of Online Activity as numbers--> </div> <!-- Trends for 30 days-->
<!-- Insights --> <!-- Insights -->
<div class="col-lg-4 mb-4 col-sm-12"> <div class="col-lg-4 mb-4 col-sm-12">
<div class="card shadow mb-4"> <div class="card shadow mb-4">
@ -842,7 +844,7 @@
class="fa fa-fw fa-long-arrow-alt-right"></i> <i class="fa fa-fw fa-long-arrow-alt-right"></i> <i
class="fa fa-fw fa-user col-amber"></i> Regular<span class="fa fa-fw fa-user col-amber"></i> Regular<span
class="float-right"><b><span class="float-right"><b><span
id="data_new_to_regular"></span></b></i></span></p> id="data_new_to_regular"></span></b></span></p>
<p><i class="fa fa-fw fa-user col-amber"></i> Regular <i <p><i class="fa fa-fw fa-user col-amber"></i> Regular <i
class="fa fa-fw fa-long-arrow-alt-right"></i> <i class="fa fa-fw fa-long-arrow-alt-right"></i> <i
class="fa fa-fw fa-user col-blue-grey"></i> Inactive<span class="fa fa-fw fa-user col-blue-grey"></i> Inactive<span
@ -1158,7 +1160,7 @@
</div> <!-- /.container-fluid --> </div> <!-- /.container-fluid -->
</div> <!-- End of Performance tab --> </div> <!-- End of Performance tab -->
<!-- Begin Plugins Overview Tab --> <!-- Begin Plugins Overview Tab -->
<div class="plugin-tabs"></div> ${tabsPlugins}
<div class="tab"></div> <div class="tab"></div>
<div class="tab"></div> <div class="tab"></div>
</div> <!-- End of Main Content --> </div> <!-- End of Main Content -->
@ -1671,19 +1673,8 @@
}); });
jsonRequest("../v1/kills?server=${serverName}", loadPlayerKills); jsonRequest("../v1/kills?server=${serverName}", loadPlayerKills);
jsonRequest("../v1/extensions?server=${serverName}", function (json, error) { $('.player-plugin-table').DataTable({
if (json) { responsive: true
$('.plugin-nav').replaceWith(json.navigation);
$('.plugin-tabs').replaceWith(json.content);
openPageFunc();
} else if (error) {
$('.plugin-nav').remove();
$('.plugin-tabs').remove();
}
$('.server-name').text('${serverName}');
$('.player-plugin-table').DataTable({
responsive: true
});
}); });
setLoadingText('Almost done..'); setLoadingText('Almost done..');