Multi proxy support (#2968)

- Change proxy server info storage to allow multiple proxies to exist in the database
- Allow naming Proxies with Server.Name config setting
- Server.IP is no longer required to be set during installation
- Change logic reliant on single proxy server to consider multiple proxies
  - If multiple proxies have webserver or export enabled, the address given by commands can be any of them.
- Network players online graph now stacks if redisbungee is not used. Individual players online graphs for different proxies can be viewed from Performance tab.
- Last Peak and All Time Peak are not given for multi-proxy networks without redisbungee since it would be expensive to calculate.
- Improved network performance tab considerably
- /plan info displays the Server UUID of the current server
- /plan server link goes to /server/UUID instead of /server/Name
- Fix join address graphs not loading if strict GROUP BY is enabled in MySQL

Affects issues:
- Close #1454
This commit is contained in:
Aurora Lahtela 2023-04-09 10:10:28 +03:00 committed by GitHub
parent f43d8f89fb
commit f7cec19372
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 650 additions and 259 deletions

View File

@ -58,4 +58,9 @@ public class BungeeSensor implements ServerSensor<Object> {
public List<String> getOnlinePlayerNames() {
return getPlayers.get().stream().map(ProxiedPlayer::getName).collect(Collectors.toList());
}
@Override
public boolean usingRedisBungee() {
return RedisCheck.isClassAvailable();
}
}

View File

@ -26,6 +26,7 @@ import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.PluginLang;
import net.playeranalytics.plugin.server.PluginLogger;
import org.jetbrains.annotations.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
@ -73,17 +74,16 @@ public class BungeeServerInfo extends ServerInfo {
@Override
public void loadServerInfo() {
logger.info(locale.getString(PluginLang.LOADING_SERVER_INFO));
checkIfDefaultIP();
this.server = fromFile.load(null).orElseGet(() -> fromDatabase.load(null)
.orElseGet(this::registerServer));
this.server = fromFile.load(null)
.orElseGet(this::registerServer);
this.server.setProxy(true); // Ensure isProxy if loaded from file
processing.submitNonCritical(this::updateStorage);
}
private void updateStorage() {
String address = addresses.getAccessAddress().orElseGet(addresses::getFallbackLocalhostAddress);
String address = getAddress();
server.setWebAddress(address);
@ -92,18 +92,6 @@ public class BungeeServerInfo extends ServerInfo {
fromFile.save(server);
}
/**
* @throws EnableException If IP setting is unset
*/
private void checkIfDefaultIP() {
String ip = serverProperties.getIp();
if ("0.0.0.0".equals(ip)) {
logger.error("IP setting still 0.0.0.0 - Configure Alternative_IP/IP that connects to the Proxy server.");
logger.info("Player Analytics partially enabled (Use /planproxy reload to reload config)");
throw new EnableException("IP setting still 0.0.0.0 - Configure Alternative_IP/IP that connects to the Proxy server.");
}
}
/**
* @throws EnableException If IP setting is unset
*/
@ -111,19 +99,22 @@ public class BungeeServerInfo extends ServerInfo {
Server proxy = createServerObject();
fromDatabase.save(proxy);
Server stored = fromDatabase.load(null)
.orElseThrow(() -> new EnableException("BungeeCord registration failed (DB)"));
Server stored = fromDatabase.load(proxy.getUuid())
.orElseThrow(() -> new EnableException("Server registration to database failed"));
fromFile.save(stored);
return stored;
}
/**
* @throws EnableException If IP setting is unset
*/
private Server createServerObject() {
ServerUUID serverUUID = generateNewUUID();
String accessAddress = addresses.getAccessAddress().orElseThrow(() -> new EnableException("Velocity can not have '0.0.0.0' or '' as an address. Set up 'Server.IP' setting."));
String accessAddress = getAddress();
return new Server(-1, serverUUID, "BungeeCord", accessAddress, true, currentVersion);
}
@Nullable
private String getAddress() {
return addresses.getAccessAddress()
.orElse(addresses.isWebserverEnabled() ? addresses.getFallbackLocalhostAddress() : null);
}
}

View File

@ -32,7 +32,8 @@ import utilities.mocks.BungeeMockComponent;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/**
@ -74,7 +75,7 @@ class BungeeSystemTest {
}
@Test
void bungeeDoesNotEnableWithDefaultIP() {
void bungeeEnablesWithDefaultIP() {
PlanSystem bungeeSystem = component.getPlanSystem();
try {
PlanConfig config = bungeeSystem.getConfigSystem().getConfig();
@ -86,8 +87,8 @@ class BungeeSystemTest {
db.setTransactionExecutorServiceProvider(MoreExecutors::newDirectExecutorService);
dbSystem.setActiveDatabase(db);
EnableException thrown = assertThrows(EnableException.class, bungeeSystem::enable);
assertEquals("IP setting still 0.0.0.0 - Configure Alternative_IP/IP that connects to the Proxy server.", thrown.getMessage());
bungeeSystem.enable();
assertTrue(bungeeSystem.isEnabled());
} finally {
bungeeSystem.disable();
}

View File

@ -105,7 +105,7 @@ public class LinkCommands {
.orElseThrow(() -> new IllegalArgumentException(locale.getString(CommandLang.FAIL_SERVER_NOT_FOUND, identifier)));
}
String address = getAddress(sender) + "/server/" + Html.encodeToURL(server.getName());
String address = getAddress(sender) + "/server/" + Html.encodeToURL(server.getUuid().toString());
sender.buildMessage()
.addPart(colors.getMainColor() + locale.getString(CommandLang.LINK_SERVER))
.apply(builder -> linkTo(builder, sender, address))
@ -126,7 +126,7 @@ public class LinkCommands {
String serversListed = dbSystem.getDatabase()
.query(ServerQueries.fetchPlanServerInformationCollection())
.stream().sorted()
.map(server -> m + server.getId().orElse(0) + "::" + t + server.getName() + "::" + s + server.getUuid() + "::" + s + server.getPlanVersion() + "\n")
.map(server -> m + server.getId().orElse(0) + "::" + t + server.getIdentifiableName() + "::" + s + server.getUuid() + "::" + s + server.getPlanVersion() + "\n")
.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
.toString();
sender.buildMessage()
@ -195,7 +195,7 @@ public class LinkCommands {
.addPart(colors.getMainColor() + locale.getString(CommandLang.LINK_NETWORK))
.apply(builder -> linkTo(builder, sender, address))
.send();
if (dbSystem.getDatabase().query(ServerQueries.fetchProxyServerInformation()).isEmpty()) {
if (dbSystem.getDatabase().query(ServerQueries.fetchProxyServers()).isEmpty()) {
throw new IllegalArgumentException(locale.getString(CommandLang.NOTIFY_NO_NETWORK));
}
}

View File

@ -107,7 +107,7 @@ public class PluginStatusCommands {
Database database = dbSystem.getDatabase();
String updateAvailable = versionChecker.isNewVersionAvailable() ? yes : no;
String proxyAvailable = database.query(ServerQueries.fetchProxyServerInformation()).isPresent() ? yes : no;
String proxyAvailable = database.query(ServerQueries.fetchProxyServers()).isEmpty() ? no : yes;
String[] messages = {

View File

@ -0,0 +1,65 @@
/*
* 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.domain.datatransfer.graphs;
import java.util.List;
import java.util.Objects;
/**
* Represents multiple graphs of same type.
*
* @author AuroraLS3
*/
public class GraphCollection<T> {
private final List<T> graphs;
private final String color;
public GraphCollection(List<T> graphs, String color) {
this.graphs = graphs;
this.color = color;
}
public List<T> getGraphs() {
return graphs;
}
public String getColor() {
return color;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GraphCollection<?> that = (GraphCollection<?>) o;
return Objects.equals(getGraphs(), that.getGraphs()) && Objects.equals(getColor(), that.getColor());
}
@Override
public int hashCode() {
return Objects.hash(getGraphs(), getColor());
}
@Override
public String toString() {
return "GraphCollection{" +
"graphs=" + graphs +
", color='" + color + '\'' +
'}';
}
}

View File

@ -0,0 +1,67 @@
/*
* 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.domain.datatransfer.graphs;
import com.djrapitops.plan.delivery.domain.datatransfer.ServerDto;
import java.util.List;
import java.util.Objects;
/**
* Represents a line graph of some server so that they can be stacked.
*
* @author AuroraLS3
*/
public class ServerSpecificLineGraph {
private final List<Double[]> points;
private final ServerDto server;
public ServerSpecificLineGraph(List<Double[]> points, ServerDto server) {
this.points = points;
this.server = server;
}
public List<Double[]> getPoints() {
return points;
}
public ServerDto getServer() {
return server;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ServerSpecificLineGraph that = (ServerSpecificLineGraph) o;
return Objects.equals(getPoints(), that.getPoints()) && Objects.equals(getServer(), that.getServer());
}
@Override
public int hashCode() {
return Objects.hash(getPoints(), getServer());
}
@Override
public String toString() {
return "ServerSpecificLineGraph{" +
"points=" + points +
", server=" + server +
'}';
}
}

View File

@ -75,7 +75,7 @@ public class ExportScheduler extends PluginRunnable {
private void scheduleExport() {
Database database = dbSystem.getDatabase();
boolean hasProxy = database.query(ServerQueries.fetchProxyServerInformation()).isPresent();
boolean hasProxy = !database.query(ServerQueries.fetchProxyServers()).isEmpty();
if (serverInfo.getServer().isNotProxy() && hasProxy) {
return;
}

View File

@ -157,6 +157,7 @@ public class NetworkPageExporter extends FileExporter {
"network/sessionsOverview",
"network/playerbaseOverview",
"graph?type=playersOnline&server=" + serverUUID,
"graph?type=playersOnlineProxies",
"graph?type=uniqueAndNew",
"graph?type=hourlyUniqueAndNew",
"graph?type=serverPie",

View File

@ -118,12 +118,32 @@ public class JSONFactory {
Database database = dbSystem.getDatabase();
ServerUUID mainServerUUID = database.query(ServerQueries.fetchProxyServerInformation()).map(Server::getUuid).orElse(serverInfo.getServerUUID());
Map<UUID, ExtensionTabData> pluginData = database.query(new ExtensionServerTableDataQuery(mainServerUUID, xMostRecentPlayers));
List<ServerUUID> mainServerUUIDs = database.query(ServerQueries.fetchProxyServers())
.stream()
.map(Server::getUuid)
.collect(Collectors.toList());
if (mainServerUUIDs.isEmpty()) mainServerUUIDs.add(serverInfo.getServerUUID());
Map<UUID, ExtensionTabData> allPluginData = new HashMap<>();
for (ServerUUID serverUUID : mainServerUUIDs) {
Map<UUID, ExtensionTabData> pluginData = database.query(new ExtensionServerTableDataQuery(serverUUID, xMostRecentPlayers));
for (Map.Entry<UUID, ExtensionTabData> entry : pluginData.entrySet()) {
UUID playerUUID = entry.getKey();
ExtensionTabData dataFromServer = entry.getValue();
ExtensionTabData alreadyIncludedData = allPluginData.get(playerUUID);
if (alreadyIncludedData == null) {
allPluginData.put(playerUUID, dataFromServer);
} else {
alreadyIncludedData.combine(dataFromServer);
}
}
}
return new PlayersTableJSONCreator(
database.query(new NetworkTablePlayersQuery(System.currentTimeMillis(), playtimeThreshold, xMostRecentPlayers)),
pluginData,
allPluginData,
openPlayerLinksInNewTab,
formatters, locale,
true // players page

View File

@ -20,6 +20,9 @@ import com.djrapitops.plan.delivery.domain.DateMap;
import com.djrapitops.plan.delivery.domain.DateObj;
import com.djrapitops.plan.delivery.domain.JoinAddressCount;
import com.djrapitops.plan.delivery.domain.JoinAddressCounts;
import com.djrapitops.plan.delivery.domain.datatransfer.ServerDto;
import com.djrapitops.plan.delivery.domain.datatransfer.graphs.GraphCollection;
import com.djrapitops.plan.delivery.domain.datatransfer.graphs.ServerSpecificLineGraph;
import com.djrapitops.plan.delivery.domain.mutators.MutatorFunctions;
import com.djrapitops.plan.delivery.domain.mutators.PingMutator;
import com.djrapitops.plan.delivery.domain.mutators.TPSMutator;
@ -59,10 +62,7 @@ import net.playeranalytics.plugin.scheduling.TimeAmount;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -495,4 +495,22 @@ public class GraphJSONCreator {
.put("join_addresses_by_date", joinAddressCounts)
.build();
}
public GraphCollection<ServerSpecificLineGraph> proxyPlayersOnlineGraphs() {
Database db = dbSystem.getDatabase();
long now = System.currentTimeMillis();
long halfYearAgo = now - TimeUnit.DAYS.toMillis(180L);
List<ServerSpecificLineGraph> proxyGraphs = new ArrayList<>();
for (Server proxy : db.query(ServerQueries.fetchProxyServers())) {
ServerUUID proxyUUID = proxy.getUuid();
List<Double[]> points = Lists.map(
db.query(TPSQueries.fetchPlayersOnlineOfServer(halfYearAgo, now, proxyUUID)),
point -> Point.fromDateObj(point).toArray()
);
proxyGraphs.add(new ServerSpecificLineGraph(points, ServerDto.fromServer(proxy)));
}
return new GraphCollection<>(proxyGraphs, theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE));
}
}

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.properties.ServerProperties;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.ExportSettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
@ -78,7 +79,11 @@ public class Addresses {
public Optional<String> getAccessAddress() {
WebServer webServer = this.webserver.get();
if (!webServer.isEnabled()) {
return Optional.of(getFallbackExternalAddress());
if (config.isTrue(ExportSettings.SERVER_PAGE)) {
return Optional.of(getFallbackExternalAddress());
} else {
return Optional.empty();
}
}
return getIP().map(ip -> webServer.getProtocol() + "://" + ip);
}
@ -100,12 +105,15 @@ public class Addresses {
}
public Optional<String> getProxyServerAddress() {
return dbSystem.getDatabase().query(ServerQueries.fetchProxyServerInformation()).map(Server::getWebAddress)
.filter(this::isValidAddress);
return dbSystem.getDatabase().query(ServerQueries.fetchProxyServers())
.stream()
.map(Server::getWebAddress)
.filter(this::isValidAddress)
.findAny();
}
private boolean isValidAddress(String address) {
return !address.isEmpty() && !"0.0.0.0".equals(address);
return address != null && !address.isEmpty() && !"0.0.0.0".equals(address);
}
public Optional<String> getServerPropertyIP() {
@ -113,6 +121,10 @@ public class Addresses {
return isValidAddress(ip) ? Optional.of(ip) : Optional.empty();
}
public boolean isWebserverEnabled() {
return webserver.get().isEnabled();
}
public String getBasePath(String address) {
String basePath = address
.replace("http://", "")

View File

@ -32,6 +32,7 @@ public enum DataID {
GRAPH_PERFORMANCE,
GRAPH_OPTIMIZED_PERFORMANCE,
GRAPH_ONLINE,
GRAPH_ONLINE_PROXIES,
GRAPH_UNIQUE_NEW,
GRAPH_HOURLY_UNIQUE_NEW,
GRAPH_CALENDAR,

View File

@ -91,6 +91,7 @@ public class GraphsJSONResolver extends JSONResolver {
@ExampleObject(value = "performance", description = "Deprecated, use optimizedPerformance"),
@ExampleObject("optimizedPerformance"),
@ExampleObject("playersOnline"),
@ExampleObject("playersOnlineProxies"),
@ExampleObject("uniqueAndNew"),
@ExampleObject("hourlyUniqueAndNew"),
@ExampleObject("serverCalendar"),
@ -160,6 +161,8 @@ public class GraphsJSONResolver extends JSONResolver {
return DataID.GRAPH_OPTIMIZED_PERFORMANCE;
case "playersOnline":
return DataID.GRAPH_ONLINE;
case "playersOnlineProxies":
return DataID.GRAPH_ONLINE_PROXIES;
case "uniqueAndNew":
return DataID.GRAPH_UNIQUE_NEW;
case "hourlyUniqueAndNew":
@ -241,6 +244,8 @@ public class GraphsJSONResolver extends JSONResolver {
return graphJSON.playerHostnamePieJSONAsMap();
case GRAPH_WORLD_MAP:
return graphJSON.geolocationGraphsJSONAsMap();
case GRAPH_ONLINE_PROXIES:
return graphJSON.proxyPlayersOnlineGraphs();
case JOIN_ADDRESSES_BY_DAY:
try {
return graphJSON.joinAddressesByDay(

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.delivery.domain.datatransfer.ServerDto;
import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.gathering.ServerSensor;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
@ -44,11 +45,13 @@ import java.util.stream.Collectors;
public class NetworkMetadataJSONResolver implements NoAuthResolver {
private final ServerInfo serverInfo;
private final ServerSensor<?> serverSensor;
private final DBSystem dbSystem;
@Inject
public NetworkMetadataJSONResolver(ServerInfo serverInfo, DBSystem dbSystem) {
public NetworkMetadataJSONResolver(ServerInfo serverInfo, ServerSensor<?> serverSensor, DBSystem dbSystem) {
this.serverInfo = serverInfo;
this.serverSensor = serverSensor;
this.dbSystem = dbSystem;
}
@ -70,6 +73,7 @@ public class NetworkMetadataJSONResolver implements NoAuthResolver {
.sorted()
.collect(Collectors.toList()))
.put("currentServer", ServerDto.fromServer(serverInfo.getServer()))
.put("usingRedisBungee", serverSensor.usingRedisBungee())
.build())
.build();
}

View File

@ -60,4 +60,8 @@ public interface ServerSensor<W> {
default List<String> getOnlinePlayerNames() {
return Collections.emptyList();
}
default boolean usingRedisBungee() {
return false;
}
}

View File

@ -62,12 +62,15 @@ public class Server implements Comparable<Server> {
return name;
}
public static String getIdentifiableName(String name, int id) {
return !"Plan".equalsIgnoreCase(name) ? name : "Server " + id;
public static String getIdentifiableName(String name, int id, boolean proxy) {
String serverPrefix = proxy ? "Proxy " : "Server ";
return "Plan".equalsIgnoreCase(name) || "Proxy".equalsIgnoreCase(name)
? serverPrefix + id
: name;
}
public String getIdentifiableName() {
return getIdentifiableName(name, id);
return getIdentifiableName(name, id, proxy);
}
public void setName(String name) {

View File

@ -45,8 +45,7 @@ public class ServerDBLoader implements ServerLoader {
public Optional<Server> load(ServerUUID serverUUID) {
try {
if (serverUUID == null) {
// Assumes that we want the server that has single entry in the database, the proxy
return dbSystem.getDatabase().query(ServerQueries.fetchProxyServerInformation());
throw new EnableException("Attempted to load a server with null UUID (Old behavior that is no longer supported)");
}
return dbSystem.getDatabase().query(

View File

@ -84,7 +84,7 @@ public class ServerFileLoader implements ServerLoader {
ServerUUID serverUUID = ServerUUID.fromString(serverUUIDString);
String name = config.getNode(PluginSettings.SERVER_NAME.getPath())
.map(ConfigNode::getString)
.orElse("Proxy");
.orElse("Plan");
String address = serverInfoConfig.get().getString("Server.Web_address");
return Optional.of(new Server(id, serverUUID, name, address, false, currentVersion));

View File

@ -373,10 +373,12 @@ public enum HtmlLang implements Lang {
PLAYER_COUNT("html.label.unit.playerCount", "Player Count"),
WARNING_NO_GAME_SERVERS("html.description.noGameServers", "Some data requires Plan to be installed on game servers."),
WARNING_PERFORMANCE_NO_GAME_SERVERS("html.description.performanceNoGameServers", "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop."),
WARNING_NO_GEOLOCATIONS("html.description.noGeolocations", "Geolocation gathering needs to be enabled in the config (Accept GeoLite2 EULA)."),
WARNING_NO_SPONGE_CHUNKS("html.description.noSpongeChunks", "Chunks unavailable on Sponge"),
WARNING_NO_DATA_24H("html.description.noData24h", "Server has not sent data for over 24 hours"),
WARNING_NO_DATA_30D("html.description.noData30d", "Server has not sent data for over 30 days"),
WARNING_NO_DATA_24H("html.description.noData24h", "Server has not sent data for over 24 hours."),
WARNING_NO_DATA_7D("html.description.noData7d", "Server has not sent data for over 7 days."),
WARNING_NO_DATA_30D("html.description.noData30d", "Server has not sent data for over 30 days."),
EXPORTED_TITLE("html.label.exported", "Data export time");
private final String key;

View File

@ -28,6 +28,7 @@ import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
import com.djrapitops.plan.storage.database.sql.tables.*;
import com.djrapitops.plan.utilities.dev.Untrusted;
import org.apache.commons.lang3.StringUtils;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.Nullable;
import javax.inject.Inject;
@ -50,17 +51,19 @@ public class PluginBooleanGroupFilter extends MultiOptionFilter {
}
private static Query<List<PluginBooleanOption>> pluginBooleanOptionsQuery() {
@Language("SQL")
String selectOptions = SELECT + DISTINCT +
"server." + ServerTable.ID + " as server_id," +
"server." + ServerTable.NAME + " as server_name," +
"server." + ServerTable.PROXY + " as is_proxy," +
"plugin." + ExtensionPluginTable.PLUGIN_NAME + " as plugin_name," +
"provider." + ExtensionProviderTable.TEXT + " as provider_text" +
FROM + ServerTable.TABLE_NAME + " server" +
INNER_JOIN + ExtensionPluginTable.TABLE_NAME + " plugin on plugin." + ExtensionPluginTable.SERVER_UUID + "=server." + ServerTable.SERVER_UUID +
INNER_JOIN + ExtensionProviderTable.TABLE_NAME + " provider on provider." + ExtensionProviderTable.PLUGIN_ID + "=plugin." + ExtensionPluginTable.ID +
INNER_JOIN + ExtensionPlayerValueTable.TABLE_NAME + " value on value." + ExtensionPlayerValueTable.PROVIDER_ID + "=provider." + ExtensionProviderTable.ID +
WHERE + "value." + ExtensionPlayerValueTable.BOOLEAN_VALUE + " IS NOT NULL" +
ORDER_BY + "server_name ASC, plugin_name ASC, provider_text ASC";
INNER_JOIN + ExtensionPlayerValueTable.TABLE_NAME + " v on v." + ExtensionPlayerValueTable.PROVIDER_ID + "=provider." + ExtensionProviderTable.ID +
WHERE + "v." + ExtensionPlayerValueTable.BOOLEAN_VALUE + " IS NOT NULL" +
ORDER_BY + "server_name, plugin_name, provider_text";
return new QueryAllStatement<>(selectOptions) {
@Override
public List<PluginBooleanOption> processResults(ResultSet set) throws SQLException {
@ -68,10 +71,11 @@ public class PluginBooleanGroupFilter extends MultiOptionFilter {
while (set.next()) {
int serverId = set.getInt("server_id");
String serverName = set.getString("server_name");
boolean proxy = set.getBoolean("is_proxy");
String pluginName = set.getString("plugin_name");
String providerText = set.getString("provider_text");
options.add(new PluginBooleanOption(
Server.getIdentifiableName(serverName, serverId),
Server.getIdentifiableName(serverName, serverId, proxy),
pluginName,
providerText
));

View File

@ -85,6 +85,7 @@ public class PluginGroupsFilter extends MultiOptionFilter {
"pl." + ExtensionPluginTable.PLUGIN_NAME + " as plugin_name," +
"s." + ServerTable.NAME + " as server_name," +
"s." + ServerTable.ID + " as server_id," +
"s." + ServerTable.PROXY + " as is_proxy," +
"pl." + ExtensionPluginTable.SERVER_UUID + " as server_uuid," +
"pr." + ExtensionProviderTable.PROVIDER_NAME + " as provider_name," +
"gr." + ExtensionGroupsTable.GROUP_NAME + " as group_name" +
@ -106,7 +107,8 @@ public class PluginGroupsFilter extends MultiOptionFilter {
ProviderIdentifier identifier = new ProviderIdentifier(serverUUID, plugin, provider);
identifier.setServerName(Server.getIdentifiableName(
set.getString("server_name"),
set.getInt("server_id")
set.getInt("server_id"),
set.getBoolean("is_proxy")
));
String group = set.getString("group_name");

View File

@ -176,7 +176,7 @@ public class JoinAddressQueries {
WHERE + SessionsTable.SERVER_ID + "=" + ServerTable.SELECT_SERVER_ID +
AND + SessionsTable.SESSION_START + ">?" +
AND + SessionsTable.SESSION_START + "<=?" +
GROUP_BY + "date,j." + JoinAddressTable.ID;
GROUP_BY + "date,j." + JoinAddressTable.JOIN_ADDRESS;
return db.query(new QueryStatement<>(selectAddresses, 1000) {
@Override
@ -219,7 +219,7 @@ public class JoinAddressQueries {
LEFT_JOIN + JoinAddressTable.TABLE_NAME + " j on s." + SessionsTable.JOIN_ADDRESS_ID + "=j." + JoinAddressTable.ID +
WHERE + SessionsTable.SESSION_START + ">?" +
AND + SessionsTable.SESSION_START + "<=?" +
GROUP_BY + "date,j." + JoinAddressTable.ID;
GROUP_BY + "date,j." + JoinAddressTable.JOIN_ADDRESS;
return db.query(new QueryStatement<>(selectAddresses, 1000) {
@Override

View File

@ -170,7 +170,7 @@ public class KillQueries {
new PlayerKill.Killer(killer, killerName),
new PlayerKill.Victim(victim, victimName, set.getLong("victim_" + UsersTable.REGISTERED)),
new ServerIdentifier(ServerUUID.fromString(set.getString(KillsTable.SERVER_UUID)),
Server.getIdentifiableName(set.getString("server_name"), set.getInt("server_id"))
Server.getIdentifiableName(set.getString("server_name"), set.getInt("server_id"), false)
), weapon, date
));
}

View File

@ -146,32 +146,29 @@ public class ServerQueries {
};
}
public static Query<Optional<Server>> fetchProxyServerInformation() {
public static Query<List<Server>> fetchProxyServers() {
String sql = SELECT + '*' + FROM + ServerTable.TABLE_NAME +
WHERE + ServerTable.INSTALLED + "=?" +
AND + ServerTable.PROXY + "=?" +
LIMIT + '1';
return new QueryStatement<>(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setBoolean(1, true);
statement.setBoolean(2, true);
}
AND + ServerTable.PROXY + "=?";
return db -> db.queryList(sql, set ->
new Server(
set.getInt(ServerTable.ID),
ServerUUID.fromString(set.getString(ServerTable.SERVER_UUID)),
set.getString(ServerTable.NAME),
set.getString(ServerTable.WEB_ADDRESS),
set.getBoolean(ServerTable.PROXY),
set.getString(ServerTable.PLAN_VERSION)
), true, true
);
}
@Override
public Optional<Server> processResults(ResultSet set) throws SQLException {
if (set.next()) {
return Optional.of(new Server(
set.getInt(ServerTable.ID),
ServerUUID.fromString(set.getString(ServerTable.SERVER_UUID)),
set.getString(ServerTable.NAME),
set.getString(ServerTable.WEB_ADDRESS),
set.getBoolean(ServerTable.PROXY),
set.getString(ServerTable.PLAN_VERSION)));
}
return Optional.empty();
}
};
public static Query<List<ServerUUID>> fetchProxyServerUUIDs() {
String sql = SELECT + ServerTable.SERVER_UUID + FROM + ServerTable.TABLE_NAME +
WHERE + ServerTable.INSTALLED + "=?" +
AND + ServerTable.PROXY + "=?";
return db -> db.queryList(sql, set -> ServerUUID.fromString(set.getString(ServerTable.SERVER_UUID)),
true, true
);
}
public static Query<List<String>> fetchGameServerNames() {
@ -185,7 +182,7 @@ public class ServerQueries {
public List<String> processResults(ResultSet set) throws SQLException {
List<String> names = new ArrayList<>();
while (set.next()) {
names.add(Server.getIdentifiableName(set.getString(ServerTable.NAME), set.getInt(ServerTable.ID)));
names.add(Server.getIdentifiableName(set.getString(ServerTable.NAME), set.getInt(ServerTable.ID), false));
}
return names;
}
@ -194,7 +191,7 @@ public class ServerQueries {
public static Query<Map<ServerUUID, String>> fetchServerNames() {
String sql = Select.from(ServerTable.TABLE_NAME,
ServerTable.ID, ServerTable.SERVER_UUID, ServerTable.NAME)
ServerTable.ID, ServerTable.SERVER_UUID, ServerTable.NAME, ServerTable.PROXY)
.toString();
return new QueryAllStatement<>(sql) {
@ -203,7 +200,9 @@ public class ServerQueries {
Map<ServerUUID, String> names = new HashMap<>();
while (set.next()) {
ServerUUID serverUUID = ServerUUID.fromString(set.getString(ServerTable.SERVER_UUID));
names.put(serverUUID, Server.getIdentifiableName(set.getString(ServerTable.NAME), set.getInt(ServerTable.ID)));
names.put(serverUUID, Server.getIdentifiableName(set.getString(ServerTable.NAME),
set.getInt(ServerTable.ID),
set.getBoolean(ServerTable.PROXY)));
}
return names;
}

View File

@ -186,7 +186,8 @@ public class SessionQueries {
ServerName serverName = new ServerName(
Server.getIdentifiableName(
set.getString("server_name"),
set.getInt("server_id")
set.getInt("server_id"),
false
));
extraData.put(ServerName.class, serverName);
@ -777,7 +778,8 @@ public class SessionQueries {
while (set.next()) {
String name = Server.getIdentifiableName(
set.getString(ServerTable.NAME),
set.getInt(ServerTable.ID)
set.getInt(ServerTable.ID),
false
);
playtimePerServer.put(name, set.getLong("playtime"));
}

View File

@ -58,9 +58,10 @@ public class ExtensionDisableOnGameServerTask extends TaskSystem.Task {
private void checkAndDisableProxyExtensions(String pluginName) {
Database db = dbSystem.getDatabase();
db.query(ServerQueries.fetchProxyServerInformation())
db.query(ServerQueries.fetchProxyServers())
.stream()
.map(Server::getUuid)
.ifPresent(proxyUUID -> checkAndDisableProxyExtension(proxyUUID, pluginName));
.forEach(proxyUUID -> checkAndDisableProxyExtension(proxyUUID, pluginName));
}
private void checkAndDisableProxyExtension(ServerUUID proxyUUID, String pluginName) {

View File

@ -5,6 +5,7 @@
# https://github.com/plan-player-analytics/Plan/wiki/Bukkit-Configuration
# -----------------------------------------------------
Server:
ServerName: Plan
IP: 0.0.0.0
Network:
Name: Plan

View File

@ -104,7 +104,7 @@ class ExportRegressionTest {
String address = "http://" + webserver.getHost() + ":" + webserver.getMappedPort(8080)
+ (endpoint.startsWith("/") ? endpoint : '/' + endpoint);
List<LogEntry> logs = getLogsAfterRequestToAddress(driver, address);
assertNoLogs(logs);
assertNoLogs(logs, endpoint);
})
).collect(Collectors.toList());
}

View File

@ -61,13 +61,13 @@ public class ExportTestUtilities {
/* Static utility method class */
}
public static void assertNoLogs(List<LogEntry> logs) {
public static void assertNoLogs(List<LogEntry> logs, String endpoint) {
List<String> loggedLines = logs.stream()
.map(log -> "\n" + log.getLevel().getName() + " " + log.getMessage())
.filter(log -> !StringUtils.containsAny(log,
"fonts.gstatic.com", "fonts.googleapis.com", "cdn.jsdelivr.net"
)).toList();
assertTrue(loggedLines.isEmpty(), () -> "Browser console included " + loggedLines.size() + " logs: " + loggedLines);
assertTrue(loggedLines.isEmpty(), () -> "Loading " + endpoint + ", Browser console included " + loggedLines.size() + " logs: " + loggedLines);
}
public static void assertNoLogsExceptFaviconError(List<LogEntry> logs) {

View File

@ -28,6 +28,7 @@ import utilities.OptionalAssert;
import utilities.TestConstants;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -53,8 +54,8 @@ public interface ServerQueriesTest extends DatabaseTestPreparer {
@Test
default void bungeeInformationIsStored() {
Optional<Server> bungeeInfo = db().query(ServerQueries.fetchProxyServerInformation());
assertFalse(bungeeInfo.isPresent());
List<Server> proxies = db().query(ServerQueries.fetchProxyServers());
assertTrue(proxies.isEmpty());
ServerUUID bungeeUUID = ServerUUID.randomUUID();
Server bungeeCord = new Server(bungeeUUID, "BungeeCord", "Random:1234", TestConstants.VERSION);
@ -65,9 +66,9 @@ public interface ServerQueriesTest extends DatabaseTestPreparer {
bungeeCord.setId(2);
bungeeInfo = db().query(ServerQueries.fetchProxyServerInformation());
assertTrue(bungeeInfo.isPresent());
assertEquals(bungeeCord, bungeeInfo.get());
proxies = db().query(ServerQueries.fetchProxyServers());
assertFalse(proxies.isEmpty());
assertEquals(List.of(bungeeCord), proxies);
Optional<Server> found = db().query(ServerQueries.fetchServerMatchingIdentifier(bungeeUUID));
OptionalAssert.equals(bungeeCord.getWebAddress(), found.map(Server::getWebAddress));

View File

@ -71,7 +71,7 @@ public class TestSettings {
public static Collection<Setting> getProxySettings() throws IllegalAccessException {
List<Setting> settings = new ArrayList<>();
for (Class settingKeyClass : new Class[]{
for (Class<?> settingKeyClass : new Class[]{
DatabaseSettings.class,
DisplaySettings.class,
ExportSettings.class,
@ -84,7 +84,6 @@ public class TestSettings {
settings.addAll(FieldFetcher.getPublicStaticFields(settingKeyClass, Setting.class));
}
// Server settings contained in the key classes, remove
settings.remove(PluginSettings.SERVER_NAME);
settings.remove(PluginSettings.PROXY_COPY_CONFIG);
settings.remove(DatabaseSettings.TYPE);
settings.remove(DisplaySettings.WORLD_ALIASES);

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, {useEffect, useMemo, useState} from 'react';
import CardTabs from "../../CardTabs";
import {
faDragon,
@ -39,41 +39,7 @@ const PerformanceGraphsCard = ({data}) => {
const {t} = useTranslation();
const {networkMetadata} = useMetadata();
if (!data || !Object.values(data).length) return <CardLoader/>
const zones = {
tps: [{
value: data.zones.tpsThresholdMed,
color: data.colors.low
}, {
value: data.zones.tpsThresholdHigh,
color: data.colors.med
}, {
value: 30,
color: data.colors.high
}],
disk: [{
value: data.zones.diskThresholdMed,
color: data.colors.low
}, {
value: data.zones.diskThresholdHigh,
color: data.colors.med
}, {
value: Number.MAX_VALUE,
color: data.colors.high
}]
};
const serverData = [];
for (let i = 0; i < data.servers.length; i++) {
const server = data.servers[i];
const values = data.values[i];
serverData.push({
serverName: server.serverName,
values
});
}
const series = {
const [performanceSeries, setPerformanceSeries] = useState({
players: [],
tps: [],
cpu: [],
@ -81,75 +47,148 @@ const PerformanceGraphsCard = ({data}) => {
entities: [],
chunks: [],
disk: []
}
});
const spline = 'spline';
useEffect(() => {
if (!data?.zones) return;
for (const server of serverData) {
series.players.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.playersOnline, color: data.colors.playersOnline, yAxis: 0
});
series.tps.push({
name: server.serverName, type: spline, tooltip: tooltip.twoDecimals,
data: server.values.tps, color: data.colors.high, zones: zones.tps, yAxis: 0
});
series.cpu.push({
name: server.serverName, type: spline, tooltip: tooltip.twoDecimals,
data: server.values.cpu, color: data.colors.cpu, yAxis: 0
});
series.ram.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.ram, color: data.colors.ram, yAxis: 0
});
series.entities.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.entities, color: data.colors.entities, yAxis: 0
});
series.chunks.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.chunks, color: data.colors.chunks, yAxis: 0
});
series.disk.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.disk, color: data.colors.high, zones: zones.disk, yAxis: 0
});
}
const zones = {
tps: [{
value: data.zones.tpsThresholdMed,
color: data.colors.low
}, {
value: data.zones.tpsThresholdHigh,
color: data.colors.med
}, {
value: 30,
color: data.colors.high
}],
disk: [{
value: data.zones.diskThresholdMed,
color: data.colors.low
}, {
value: data.zones.diskThresholdHigh,
color: data.colors.med
}, {
value: Number.MAX_VALUE,
color: data.colors.high
}]
};
const serverData = [];
for (let i = 0; i < data.servers.length; i++) {
const server = data.servers[i];
const values = data.values[i];
serverData.push({
serverName: server.serverName,
values
});
}
const series = {
players: [],
tps: [],
cpu: [],
ram: [],
entities: [],
chunks: [],
disk: []
}
const spline = 'spline';
const changeColor = (colorHex, index) => {
// TODO Convert color somehow using index
return colorHex;
}
const minuteResolution = point => {
// Ensure that the points can be stacked by moving data to minute level
point[0] -= (point[0] % 60000);
return point;
}
serverData.forEach((server, i) => {
const playersOnlineColor = changeColor(data.colors.playersOnline, i);
const tpsColor = changeColor(data.colors.high, i);
const tpsZone = [...zones.tps]
tpsZone.forEach(zone => zone.color = changeColor(zone.color, i));
const cpuColor = changeColor(data.colors.cpu, i);
const ramColors = changeColor(data.colors.ram, i);
const entitiesColor = changeColor(data.colors.entities, i);
const chunksColor = changeColor(data.colors.chunks, i);
const diskColor = changeColor(data.colors.high, i);
const diskZones = [...zones.disk];
diskZones.forEach(zone => zone.color = changeColor(zone.color, i));
series.players.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.playersOnline.map(minuteResolution), color: playersOnlineColor, yAxis: 0
});
series.tps.push({
name: server.serverName, type: spline, tooltip: tooltip.twoDecimals,
data: server.values.tps.map(minuteResolution), color: tpsColor, zones: tpsZone, yAxis: 0
});
series.cpu.push({
name: server.serverName, type: spline, tooltip: tooltip.twoDecimals,
data: server.values.cpu.map(minuteResolution), color: cpuColor, yAxis: 0
});
series.ram.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.ram.map(minuteResolution), color: ramColors, yAxis: 0
});
series.entities.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.entities.map(minuteResolution), color: entitiesColor, yAxis: 0
});
series.chunks.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.chunks.map(minuteResolution), color: chunksColor, yAxis: 0
});
series.disk.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.disk.map(minuteResolution), color: diskColor, zones: diskZones, yAxis: 0
});
});
setPerformanceSeries(series);
}, [data, setPerformanceSeries])
const tabs = useMemo(() => [
{
name: t('html.label.playersOnline'), icon: faUser, color: 'light-blue', href: 'players-online',
element: <Tab data={performanceSeries.players} yAxis={yAxisConfigurations.PLAYERS_ONLINE}/>
}, {
name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps',
element: <Tab data={performanceSeries.tps} yAxis={yAxisConfigurations.TPS}/>
}, {
name: t('html.label.cpu'), icon: faTachometerAlt, color: 'amber', href: 'cpu',
element: <Tab data={performanceSeries.cpu} yAxis={yAxisConfigurations.CPU}/>
}, {
name: t('html.label.ram'), icon: faMicrochip, color: 'light-green', href: 'ram',
element: <Tab data={performanceSeries.ram} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
}, {
name: t('html.label.entities'), icon: faDragon, color: 'purple', href: 'entities',
element: <Tab data={performanceSeries.entities} yAxis={yAxisConfigurations.ENTITIES}/>
}, {
name: t('html.label.loadedChunks'), icon: faMap, color: 'blue-grey', href: 'chunks',
element: <Tab data={performanceSeries.chunks} yAxis={yAxisConfigurations.CHUNKS}/>
}, {
name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk',
element: <Tab data={performanceSeries.disk} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
}, {
name: t('html.label.ping'), icon: faSignal, color: 'amber', href: 'ping',
element: networkMetadata ? <PingTab identifier={networkMetadata.currentServer.serverUUID}/> :
<ChartLoader/>
},
], [performanceSeries, networkMetadata, t]);
if (!data || !Object.values(data).length) return <CardLoader/>
if (data.errors.length) {
return <ErrorViewCard error={data.errors[0]}/>
}
return (
<Card>
<CardTabs tabs={[
{
name: t('html.label.playersOnline'), icon: faUser, color: 'light-blue', href: 'players-online',
element: <Tab data={series.players} yAxis={yAxisConfigurations.PLAYERS_ONLINE}/>
}, {
name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps',
element: <Tab data={series.tps} yAxis={yAxisConfigurations.TPS}/>
}, {
name: t('html.label.cpu'), icon: faTachometerAlt, color: 'amber', href: 'cpu',
element: <Tab data={series.cpu} yAxis={yAxisConfigurations.CPU}/>
}, {
name: t('html.label.ram'), icon: faMicrochip, color: 'light-green', href: 'ram',
element: <Tab data={series.ram} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
}, {
name: t('html.label.entities'), icon: faDragon, color: 'purple', href: 'entities',
element: <Tab data={series.entities} yAxis={yAxisConfigurations.ENTITIES}/>
}, {
name: t('html.label.loadedChunks'), icon: faMap, color: 'blue-grey', href: 'chunks',
element: <Tab data={series.chunks} yAxis={yAxisConfigurations.CHUNKS}/>
}, {
name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk',
element: <Tab data={series.disk} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
}, {
name: t('html.label.ping'), icon: faSignal, color: 'amber', href: 'ping',
element: networkMetadata ? <PingTab identifier={networkMetadata.currentServer.serverUUID}/> :
<ChartLoader/>
},
]}/>
<CardTabs tabs={tabs}/>
</Card>
)
};

View File

@ -10,15 +10,34 @@ import {ChartLoader} from "../../../navigation/Loader";
import TimeByTimeGraph from "../../../graphs/TimeByTimeGraph";
import PlayersOnlineGraph from "../../../graphs/PlayersOnlineGraph";
import {useMetadata} from "../../../../hooks/metadataHook";
import StackedPlayersOnlineGraph from "../../../graphs/StackedPlayersOnlineGraph";
const PlayersOnlineTab = () => {
const {serverUUID} = useMetadata();
const SingleProxyPlayersOnlineGraph = ({serverUUID}) => {
const {data, loadingError} = useDataRequest(fetchPlayersOnlineGraph, [serverUUID]);
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!serverUUID || !data) return <ChartLoader/>;
return <PlayersOnlineGraph data={data}/>
}
const MultiProxyPlayersOnlineGraph = () => {
const {data, loadingError} = useDataRequest(fetchPlayersOnlineGraph, []);
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <ChartLoader/>;
return <PlayersOnlineGraph data={data}/>
return <StackedPlayersOnlineGraph data={data}/>
}
const PlayersOnlineTab = () => {
const {serverUUID, networkMetadata} = useMetadata();
if (networkMetadata.usingRedisBungee || networkMetadata.servers.filter(server => server.proxy).length === 1) {
return <SingleProxyPlayersOnlineGraph serverUUID={serverUUID}/>
} else {
return <MultiProxyPlayersOnlineGraph/>
}
}
const DayByDayTab = () => {

View File

@ -3,12 +3,24 @@ import PerformanceAsNumbersTable from "../../../table/PerformanceAsNumbersTable"
import CardHeader from "../../CardHeader";
import {faBookOpen} from "@fortawesome/free-solid-svg-icons";
import {Card} from "react-bootstrap";
import {useTranslation} from "react-i18next";
const PerformanceAsNumbersCard = ({data, servers}) => {
const {t} = useTranslation();
const noData24h = data && "Unavailable" === data.cpu_24h;
const noData7d = data && "Unavailable" === data.cpu_7d;
const noData30d = data && "Unavailable" === data.cpu_30d;
const noDataAlert = noData30d ? <p className={"alert alert-warning mb-0"}>{t('html.description.noData30d')}</p>
: (noData7d ? <p className={"alert alert-warning mb-0"}>{t('html.description.noData7d')}</p>
: (noData24h ? <p className={"alert alert-warning mb-0"}>{t('html.description.noData24h')}</p>
: ''));
const PerformanceAsNumbersCard = ({data}) => {
return (
<Card>
<CardHeader icon={faBookOpen} color="blue-grey" label={'html.label.performanceAsNumbers'}/>
<PerformanceAsNumbersTable data={data}/>
{noDataAlert}
<PerformanceAsNumbersTable data={data} servers={servers}/>
</Card>
)
};

View File

@ -15,17 +15,21 @@ import {faCalendarCheck, faClock} from "@fortawesome/free-regular-svg-icons";
import React from "react";
import {CardLoader} from "../../../navigation/Loader";
import ExtendableCardBody from "../../../layout/extension/ExtendableCardBody";
import {useMetadata} from "../../../../hooks/metadataHook";
const ServerAsNumbersCard = ({data}) => {
const {t} = useTranslation();
const {networkMetadata} = useMetadata();
if (!data) return <CardLoader/>;
if (!data || !networkMetadata) return <CardLoader/>;
const isGameServer = data.player_kills !== undefined;
const showPeaks = isGameServer || networkMetadata.usingRedisBungee || networkMetadata.servers.filter(server => server.proxy).length === 1;
return (
<Card>
<Card.Header>
<h6 className="col-black">
<Fa icon={faBookOpen}/> {data.player_kills !== undefined ? t('html.label.serverAsNumberse') : t('html.label.networkAsNumbers')}
<Fa icon={faBookOpen}/> {isGameServer ? t('html.label.serverAsNumberse') : t('html.label.networkAsNumbers')}
</h6>
</Card.Header>
<ExtendableCardBody
@ -43,13 +47,15 @@ const ServerAsNumbersCard = ({data}) => {
<Datapoint name={t('html.label.playersOnline')}
color={'blue'} icon={faUser}
value={data.online_players} bold/>
<hr/>
<Datapoint name={t('html.label.lastPeak') + ' (' + data.last_peak_date + ')'}
color={'blue'} icon={faChartLine}
value={data.last_peak_players} valueLabel={t('html.unit.players')} bold/>
<Datapoint name={t('html.label.bestPeak') + ' (' + data.best_peak_date + ')'}
color={'light-green'} icon={faChartLine}
value={data.best_peak_players} valueLabel={t('html.unit.players')} bold/>
{showPeaks && <>
<hr/>
<Datapoint name={t('html.label.lastPeak') + ' (' + data.last_peak_date + ')'}
color={'blue'} icon={faChartLine}
value={data.last_peak_players} valueLabel={t('html.unit.players')} bold/>
<Datapoint name={t('html.label.bestPeak') + ' (' + data.best_peak_date + ')'}
color={'light-green'} icon={faChartLine}
value={data.best_peak_players} valueLabel={t('html.unit.players')} bold/>
</>}
<hr/>
<Datapoint name={t('html.label.totalPlaytime')}
color={'green'} icon={faClock}

View File

@ -0,0 +1,73 @@
import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {linegraphButtons, tooltip} from "../../util/graphs";
import LineGraph from './LineGraph'
import {ChartLoader} from "../navigation/Loader";
import {useTheme} from "../../hooks/themeHook";
const StackedPlayersOnlineGraph = ({data}) => {
const {t} = useTranslation();
const {nightModeEnabled} = useTheme();
const [graphOptions, setGraphOptions] = useState({title: {text: ''},});
useEffect(() => {
if (!data) return;
const playersOnlineSeries = data.graphs.map(graph => {
return {
name: t('html.label.playersOnline') + ' (' + graph.server.serverName + ')',
type: 'areaspline',
tooltip: tooltip.zeroDecimals,
data: graph.points.map(point => {
// Ensure that the points can be stacked by moving data to minute level
point[0] -= (point[0] % 60000);
return point;
}),
color: data.color,
yAxis: 0
}
});
setGraphOptions({
title: {text: ''},
rangeSelector: {
selected: 2,
buttons: linegraphButtons
},
chart: {
zooming: {
type: 'xy'
}
},
plotOptions: {
areaspline: {
fillOpacity: nightModeEnabled ? 0.2 : 0.4,
stacking: 'normal'
}
},
legend: {
enabled: true,
},
xAxis: {
zoomEnabled: true,
title: {
enabled: false
}
},
yAxis: {
zoomEnabled: true,
title: {text: t('html.label.players')},
softMax: 2,
min: 0
},
series: playersOnlineSeries
})
}, [data, nightModeEnabled, t])
if (!data) return <ChartLoader/>;
return (
<LineGraph id="stacked-players-online-graph"
options={graphOptions}/>
)
}
export default StackedPlayersOnlineGraph

View File

@ -12,14 +12,20 @@ import {
import React from "react";
import {TableRow} from "./TableRow";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faEye} from "@fortawesome/free-regular-svg-icons";
import {faEye, faQuestionCircle} from "@fortawesome/free-regular-svg-icons";
import AsNumbersTable from "./AsNumbersTable";
import {ChartLoader} from "../navigation/Loader";
const PerformanceAsNumbersTable = ({data}) => {
const PerformanceAsNumbersTable = ({data, servers}) => {
const {t} = useTranslation();
if (!data) return <ChartLoader/>;
const dataIncludesGameServers = servers && Boolean(servers.filter(server => !server.proxy).length);
const noTPSOnProxies = !servers || dataIncludesGameServers
? ''
: <Fa icon={faQuestionCircle}
title={t('html.description.performanceNoGameServers')}/>;
return (
<AsNumbersTable
headers={[t('html.label.last30days'), t('html.label.last7days'), t('html.label.last24hours')]}
@ -52,9 +58,9 @@ const PerformanceAsNumbersTable = ({data}) => {
]}/>
<TableRow icon={faTachometerAlt} color="orange" text={t('html.label.averageTps')}
values={[
data.tps_30d,
data.tps_7d,
data.tps_24h
<>{data.tps_30d} {noTPSOnProxies}</>,
<>{data.tps_7d} {noTPSOnProxies}</>,
<>{data.tps_24h} {noTPSOnProxies}</>
]}/>
<TableRow icon={faTachometerAlt} color="amber" text={t('html.label.averageCpuUsage')}
values={[
@ -70,17 +76,17 @@ const PerformanceAsNumbersTable = ({data}) => {
]}/>
<TableRow icon={faDragon} color="purple" text={t('html.label.averageEntities')}
values={[
data.entities_30d,
data.entities_7d,
data.entities_24h
<>{data.entities_30d} {noTPSOnProxies}</>,
<>{data.entities_7d} {noTPSOnProxies}</>,
<>{data.entities_24h} {noTPSOnProxies}</>
]}/>
<TableRow icon={faMap} color="blue-grey"
text={<>{t('html.label.averageChunks')}{' '}{data.chunks_30d === 'Unavailable' ?
<Fa icon={faEye} title={t('html.description.noSpongeChunks')}/> : ''}</>}
values={[
data.chunks_30d,
data.chunks_7d,
data.chunks_24h
<>{data.chunks_30d} {noTPSOnProxies}</>,
<>{data.chunks_7d} {noTPSOnProxies}</>,
<>{data.chunks_24h} {noTPSOnProxies}</>
]}/>
<TableRow icon={faHdd} color="green"
text={t('html.label.maxFreeDisk')}

View File

@ -114,8 +114,8 @@ const fetchPlayersOnlineGraphServer = async (timestamp, identifier) => {
}
const fetchPlayersOnlineGraphNetwork = async (timestamp) => {
let url = `/v1/graph?type=playersOnline`;
if (staticSite) url = `/data/graph-playersOnline.json`;
let url = `/v1/graph?type=playersOnlineProxies`;
if (staticSite) url = `/data/graph-playersOnlineProxies.json`;
return doGetRequest(url, timestamp);
}

View File

@ -123,9 +123,31 @@ export const colorClassToBgClass = colorClass => {
return "bg-" + colorClassToColorName(colorClass);
}
export const hsxStringToArray = (hsvString) => {
const color = hsvString.substring(4, hsvString.length - 1);
const split = color.split(',');
const h = Number(split[0]);
const s = Number(split[1].substring(0, split[1].length - 1));
const x = Number(split[2].substring(0, split[2].length - 1));
return [h, s, x];
}
export const hslToHsv = ([h, s, l]) => {
const hsv1 = s * (l < 50 ? l : 100 - l) / 100;
const hsvS = hsv1 === 0 ? 0 : 2 * hsv1 / (l + hsv1) * 100;
const hsvV = l + hsv1;
return [h, hsvS, hsvV];
}
export const hsvToRgb = ([h, s, v]) => {
let r, g, b;
if (s > 1) {
h = h / 360;
s = s / 100;
v = v / 100;
}
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
@ -186,15 +208,15 @@ const rgbToHex = (component) => {
return Math.floor(component).toString(16).padStart(2, '0');
}
// From https://stackoverflow.com/a/3732187
export const withReducedSaturation = hex => {
const saturationReduction = 0.70;
// To RGB
let r = parseInt(hex.substr(1, 2), 16); // Grab the hex representation of red (chars 1-2) and convert to decimal (base 10).
let g = parseInt(hex.substr(3, 2), 16);
let b = parseInt(hex.substr(5, 2), 16);
export const hexToRgb = (hexString) => {
const r = parseInt(hexString.substring(1, 3), 16);
const g = parseInt(hexString.substring(3, 5), 16);
const b = parseInt(hexString.substring(5, 7), 16);
return [r, g, b];
}
// To HSL
// https://css-tricks.com/converting-color-spaces-in-javascript/
export const rgbToHsl = ([r, g, b]) => {
r /= 255;
g /= 255;
b /= 255;
@ -222,6 +244,15 @@ export const withReducedSaturation = hex => {
}
h /= 6;
}
return [h, s, l];
}
// From https://stackoverflow.com/a/3732187
export const withReducedSaturation = (hex, reduceSaturationPercentage) => {
const saturationReduction = reduceSaturationPercentage ? reduceSaturationPercentage : 0.70;
const rgb = hexToRgb(hex);
const [h, s, l] = rgbToHsl(rgb);
// To css property
return 'hsl(' + h * 360 + ',' + s * 100 * saturationReduction + '%,' + l * 95 + '%)';

View File

@ -23,7 +23,7 @@ const NetworkPerformance = () => {
const [selectedOptions, setSelectedOptions] = useState([]);
const [visualizedServers, setVisualizedServers] = useState([]);
const initializeServerOptions = () => {
useEffect(() => {
if (networkMetadata) {
const options = networkMetadata.servers;
setServerOptions(options);
@ -34,8 +34,7 @@ const NetworkPerformance = () => {
setSelectedOptions([indexOfProxy]);
setVisualizedServers([indexOfProxy]);
}
};
useEffect(initializeServerOptions, [networkMetadata, setVisualizedServers]);
}, [networkMetadata, setVisualizedServers]);
const applySelected = () => {
setVisualizedServers(selectedOptions);
@ -85,7 +84,8 @@ const NetworkPerformance = () => {
loadPerformanceData();
}, [loadPerformanceData, visualizedServers, updateRequested]);
const isUpToDate = visualizedServers.every((s, i) => s === selectedOptions[i]);
const isUpToDate = selectedOptions.length === visualizedServers.length && selectedOptions.every(
(s, i) => s === visualizedServers[i]);
return (
<LoadIn>
<section className={"network-performance"}>
@ -96,7 +96,8 @@ const NetworkPerformance = () => {
</ExtendableRow>
<ExtendableRow id={'row-network-performance-1'}>
<Col md={8}>
<PerformanceAsNumbersCard data={performanceData?.overview?.numbers}/>
<PerformanceAsNumbersCard data={performanceData?.overview?.numbers}
servers={performanceData.servers || []}/>
</Col>
<Col md={4}>
<Card>
@ -104,7 +105,8 @@ const NetworkPerformance = () => {
<MultiSelect options={serverOptions.map(server => server.serverName)}
selectedIndexes={selectedOptions}
setSelectedIndexes={setSelectedOptions}/>
<button className={'btn bg-transparent'} onClick={applySelected} disabled={isUpToDate}>
<button className={'btn ' + (isUpToDate ? 'bg-transparent' : 'bg-theme')}
onClick={applySelected} disabled={isUpToDate}>
{t('html.label.apply')}
</button>
</Card>

View File

@ -57,4 +57,9 @@ public class VelocitySensor implements ServerSensor<Object> {
public List<String> getOnlinePlayerNames() {
return getPlayers.get().stream().map(Player::getUsername).collect(Collectors.toList());
}
@Override
public boolean usingRedisBungee() {
return VelocityRedisCheck.isClassAvailable();
}
}

View File

@ -26,6 +26,7 @@ import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.PluginLang;
import net.playeranalytics.plugin.server.PluginLogger;
import org.jetbrains.annotations.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
@ -73,17 +74,16 @@ public class VelocityServerInfo extends ServerInfo {
@Override
public void loadServerInfo() {
logger.info(locale.getString(PluginLang.LOADING_SERVER_INFO));
checkIfDefaultIP();
this.server = fromFile.load(null).orElseGet(() -> fromDatabase.load(null)
.orElseGet(this::registerServer));
this.server = fromFile.load(null)
.orElseGet(this::registerServer);
this.server.setProxy(true); // Ensure isProxy if loaded from file
processing.submitNonCritical(this::updateStorage);
}
private void updateStorage() {
String address = addresses.getAccessAddress().orElseGet(addresses::getFallbackLocalhostAddress);
String address = getAddress();
server.setWebAddress(address);
@ -92,36 +92,27 @@ public class VelocityServerInfo extends ServerInfo {
fromFile.save(server);
}
/**
* @throws EnableException If IP setting is unset
*/
private void checkIfDefaultIP() {
String ip = serverProperties.getIp();
if ("0.0.0.0".equals(ip)) {
logger.error("IP setting still 0.0.0.0 - Configure Alternative_IP/IP that connects to the Proxy server.");
logger.info("Player Analytics partially enabled (Use /planproxy reload to reload config)");
throw new EnableException("IP setting still 0.0.0.0 - Configure Alternative_IP/IP that connects to the Proxy server.");
}
}
private Server registerServer() {
Server proxy = createServerObject();
fromDatabase.save(proxy);
Server stored = fromDatabase.load(null)
.orElseThrow(() -> new EnableException("Velocity registration failed (DB)"));
Server stored = fromDatabase.load(proxy.getUuid())
.orElseThrow(() -> new EnableException("Server registration to database failed"));
fromFile.save(stored);
return stored;
}
/**
* @throws EnableException If IP setting is unset
*/
private Server createServerObject() {
ServerUUID serverUUID = generateNewUUID();
String accessAddress = addresses.getAccessAddress().orElseThrow(() -> new EnableException("Velocity can not have '0.0.0.0' or '' as an address. Set up 'Server.IP' setting."));
String accessAddress = getAddress();
return new Server(-1, serverUUID, "Velocity", accessAddress, true, currentVersion);
}
@Nullable
private String getAddress() {
return addresses.getAccessAddress()
.orElse(addresses.isWebserverEnabled() ? addresses.getFallbackLocalhostAddress() : null);
}
}