2360/plugin versions (#3249)

* Add methods to gather plugin versions from servers
* Gathering and storage for plugin version history
* Test plugin gathering
* Test plugin metadata storage
* /v1/pluginHistory endpoint
* Plugin history tab
* Plugin history to performance tab
* Possibly fix ConfigChange.MovedValue being applied all the time
* Updated locale files
* Export pluginHistory for server page
* Add plugin history to network page
* Access control and improvements
* Remove pluginHistory from export since it now requires auth
* Fix access visibility tests
* Fix VelocitySensor during test

Affects issues:
- Close #2360
This commit is contained in:
Aurora Lahtela 2023-10-07 08:39:00 +03:00 committed by GitHub
parent 61db13626b
commit 5a2bdaf6ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1464 additions and 51 deletions

View File

@ -16,13 +16,16 @@
*/
package com.djrapitops.plan.gathering;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@ -129,4 +132,12 @@ public class BukkitSensor implements ServerSensor<World> {
.map(Player::getName)
.collect(Collectors.toList());
}
@Override
public List<PluginMetadata> getInstalledPlugins() {
return Arrays.stream(Bukkit.getPluginManager().getPlugins())
.map(Plugin::getDescription)
.map(description -> new PluginMetadata(description.getName(), description.getVersion()))
.collect(Collectors.toList());
}
}

View File

@ -26,6 +26,7 @@ import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
import com.djrapitops.plan.gathering.ShutdownHook;
import com.djrapitops.plan.gathering.timed.BukkitPingCounter;
import com.djrapitops.plan.gathering.timed.InstalledPluginGatheringTask;
import com.djrapitops.plan.gathering.timed.ServerTPSCounter;
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
import com.djrapitops.plan.settings.upkeep.ConfigStoreTask;
@ -108,4 +109,8 @@ public interface BukkitTaskModule {
@Binds
@IntoSet
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
@Binds
@IntoSet
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
}

View File

@ -17,9 +17,11 @@
package com.djrapitops.plan.gathering;
import com.djrapitops.plan.PlanBungee;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import com.djrapitops.plan.identification.properties.RedisCheck;
import com.djrapitops.plan.identification.properties.RedisPlayersOnlineSupplier;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -35,11 +37,13 @@ public class BungeeSensor implements ServerSensor<Object> {
private final IntSupplier onlinePlayerCountSupplier;
private final IntSupplier onlinePlayerCountBungee;
private final Supplier<Collection<ProxiedPlayer>> getPlayers;
private final Supplier<Collection<Plugin>> getPlugins;
@Inject
public BungeeSensor(PlanBungee plugin) {
getPlayers = plugin.getProxy()::getPlayers;
onlinePlayerCountBungee = plugin.getProxy()::getOnlineCount;
getPlugins = plugin.getProxy().getPluginManager()::getPlugins;
onlinePlayerCountSupplier = RedisCheck.isClassAvailable() ? new RedisPlayersOnlineSupplier() : onlinePlayerCountBungee;
}
@ -63,4 +67,12 @@ public class BungeeSensor implements ServerSensor<Object> {
public boolean usingRedisBungee() {
return RedisCheck.isClassAvailable();
}
@Override
public List<PluginMetadata> getInstalledPlugins() {
return getPlugins.get().stream()
.map(Plugin::getDescription)
.map(description -> new PluginMetadata(description.getName(), description.getVersion()))
.collect(Collectors.toList());
}
}

View File

@ -23,6 +23,7 @@ import com.djrapitops.plan.delivery.webserver.cache.JSONFileStorage;
import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
import com.djrapitops.plan.gathering.timed.BungeePingCounter;
import com.djrapitops.plan.gathering.timed.InstalledPluginGatheringTask;
import com.djrapitops.plan.gathering.timed.ProxyTPSCounter;
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
import com.djrapitops.plan.settings.upkeep.NetworkConfigStoreTask;
@ -87,4 +88,8 @@ public interface BungeeTaskModule {
@Binds
@IntoSet
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
@Binds
@IntoSet
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
}

View File

@ -0,0 +1,77 @@
/*
* 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;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
/**
* Represents plugin version history.
* <p>
* If version is null the plugin was uninstalled at that time.
*
* @author AuroraLS3
*/
public class PluginHistoryMetadata {
private final String name;
@Nullable
private final String version;
private final long modified;
public PluginHistoryMetadata(String name, @Nullable String version, long modified) {
this.name = name;
this.version = version;
this.modified = modified;
}
public String getName() {
return name;
}
@Nullable
public String getVersion() {
return version;
}
public long getModified() {
return modified;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PluginHistoryMetadata that = (PluginHistoryMetadata) o;
return getModified() == that.getModified() && Objects.equals(getName(), that.getName()) && Objects.equals(getVersion(), that.getVersion());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getVersion(), getModified());
}
@Override
public String toString() {
return "PluginHistoryMetadata{" +
"name='" + name + '\'' +
", version='" + version + '\'' +
", modified=" + modified +
'}';
}
}

View File

@ -55,6 +55,7 @@ public enum WebPermission implements Supplier<String>, Lang {
PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY("See Ping Per Country table"),
PAGE_NETWORK_PLAYERS("See Player list -tab"),
PAGE_NETWORK_PERFORMANCE("See network Performance tab"),
PAGE_NETWORK_PLUGIN_HISTORY("See Plugin History across the network"),
PAGE_NETWORK_PLUGINS("See Plugins tab of Proxy"),
PAGE_SERVER("See all of server page"),
@ -90,6 +91,7 @@ public enum WebPermission implements Supplier<String>, Lang {
PAGE_SERVER_PERFORMANCE("See Performance tab"),
PAGE_SERVER_PERFORMANCE_GRAPHS("See Performance graphs"),
PAGE_SERVER_PERFORMANCE_OVERVIEW("See Performance numbers"),
PAGE_SERVER_PLUGIN_HISTORY("See Plugin History"),
PAGE_SERVER_PLUGINS("See Plugins -tabs of servers"),
PAGE_PLAYER("See all of player page"),

View File

@ -0,0 +1,60 @@
/*
* 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;
import com.djrapitops.plan.delivery.domain.PluginHistoryMetadata;
import java.util.List;
import java.util.Objects;
/**
* History of plugin versions, sorted most recent first.
*
* @author AuroraLS3
*/
public class PluginHistoryDto {
private final List<PluginHistoryMetadata> history;
public PluginHistoryDto(List<PluginHistoryMetadata> history) {
this.history = history;
}
public List<PluginHistoryMetadata> getHistory() {
return history;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PluginHistoryDto that = (PluginHistoryDto) o;
return Objects.equals(getHistory(), that.getHistory());
}
@Override
public int hashCode() {
return Objects.hash(getHistory());
}
@Override
public String toString() {
return "PluginHistoryDto{" +
"history=" + history +
'}';
}
}

View File

@ -24,6 +24,7 @@ import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.delivery.webserver.resolver.json.metadata.PreferencesJSONResolver;
import com.djrapitops.plan.delivery.webserver.resolver.json.metadata.StorePreferencesJSONResolver;
import com.djrapitops.plan.delivery.webserver.resolver.json.plugins.PluginHistoryJSONResolver;
import com.djrapitops.plan.identification.Identifiers;
import dagger.Lazy;
@ -49,6 +50,7 @@ public class RootJSONResolver {
private final CompositeResolver.Builder readOnlyResourcesBuilder;
private final StorePreferencesJSONResolver storePreferencesJSONResolver;
private final PluginHistoryJSONResolver pluginHistoryJSONResolver;
private CompositeResolver resolver;
@Inject
@ -84,6 +86,7 @@ public class RootJSONResolver {
ExtensionJSONResolver extensionJSONResolver,
RetentionJSONResolver retentionJSONResolver,
PlayerJoinAddressJSONResolver playerJoinAddressJSONResolver,
PluginHistoryJSONResolver pluginHistoryJSONResolver,
PreferencesJSONResolver preferencesJSONResolver,
StorePreferencesJSONResolver storePreferencesJSONResolver,
@ -127,6 +130,7 @@ public class RootJSONResolver {
this.webServer = webServer;
// These endpoints require authentication to be enabled.
this.pluginHistoryJSONResolver = pluginHistoryJSONResolver;
this.webGroupJSONResolver = webGroupJSONResolver;
this.webGroupPermissionJSONResolver = webGroupPermissionJSONResolver;
this.webPermissionJSONResolver = webPermissionJSONResolver;
@ -149,6 +153,7 @@ public class RootJSONResolver {
.add("saveGroupPermissions", webGroupSaveJSONResolver)
.add("deleteGroup", webGroupDeleteJSONResolver)
.add("storePreferences", storePreferencesJSONResolver)
.add("pluginHistory", pluginHistoryJSONResolver)
.build();
} else {
resolver = readOnlyResourcesBuilder.build();

View File

@ -0,0 +1,100 @@
/*
* 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.webserver.resolver.json.plugins;
import com.djrapitops.plan.delivery.domain.PluginHistoryMetadata;
import com.djrapitops.plan.delivery.domain.auth.WebPermission;
import com.djrapitops.plan.delivery.domain.datatransfer.PluginHistoryDto;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.PluginMetadataQueries;
import com.djrapitops.plan.utilities.dev.Untrusted;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.List;
import java.util.Optional;
/**
* Endpoint for getting plugin version history.
*
* @author AuroraLS3
*/
@Singleton
@Path("/v1/pluginHistory")
public class PluginHistoryJSONResolver implements Resolver {
private final DBSystem dbSystem;
private final Identifiers identifiers;
@Inject
public PluginHistoryJSONResolver(DBSystem dbSystem, Identifiers identifiers) {
this.dbSystem = dbSystem;
this.identifiers = identifiers;
}
@Override
public boolean canAccess(Request request) {
return request.getUser()
.map(user -> user.hasPermission(WebPermission.PAGE_NETWORK_PLUGIN_HISTORY)
|| user.hasPermission(WebPermission.PAGE_SERVER_PLUGIN_HISTORY))
.orElse(false);
}
@Override
@Operation(
description = "Get plugin history for a server since installation of Plan.",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON,
schema = @Schema(implementation = PluginHistoryDto.class))),
},
parameters = @Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for (optional)", examples = {
@ExampleObject("Server 1"),
@ExampleObject("1"),
@ExampleObject("1fb39d2a-eb82-4868-b245-1fad17d823b3"),
}),
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@GET
public Optional<Response> resolve(@Untrusted Request request) {
return Optional.of(getResponse(request));
}
private Response getResponse(@Untrusted Request request) {
ServerUUID serverUUID = identifiers.getServerUUID(request); // Can throw BadRequestException
List<PluginHistoryMetadata> history = dbSystem.getDatabase().query(PluginMetadataQueries.getPluginHistory(serverUUID));
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(new PluginHistoryDto(history))
.build();
}
}

View File

@ -16,6 +16,8 @@
*/
package com.djrapitops.plan.gathering;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import java.util.Collections;
import java.util.List;
@ -64,4 +66,8 @@ public interface ServerSensor<W> {
default boolean usingRedisBungee() {
return false;
}
default List<PluginMetadata> getInstalledPlugins() {
return List.of();
}
}

View File

@ -0,0 +1,64 @@
/*
* 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.gathering.domain;
import java.util.Objects;
/**
* Represents a plugin that is installed on a server.
*
* @author AuroraLS3
*/
public class PluginMetadata {
private final String name;
private final String version;
public PluginMetadata(String name, String version) {
this.name = name;
this.version = version;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PluginMetadata that = (PluginMetadata) o;
return Objects.equals(getName(), that.getName()) && Objects.equals(getVersion(), that.getVersion());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getVersion());
}
@Override
public String toString() {
return "PluginMetadata{" +
"name='" + name + '\'' +
", version='" + version + '\'' +
'}';
}
}

View File

@ -0,0 +1,110 @@
/*
* 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.gathering.timed;
import com.djrapitops.plan.PlanSystem;
import com.djrapitops.plan.TaskSystem;
import com.djrapitops.plan.gathering.ServerSensor;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.PluginMetadataQueries;
import com.djrapitops.plan.storage.database.sql.tables.PluginVersionTable;
import com.djrapitops.plan.storage.database.transactions.events.StorePluginVersionsTransaction;
import net.playeranalytics.plugin.scheduling.RunnableFactory;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Gathers information about plugins that have been installed and their version history.
*
* @author AuroraLS3
*/
@Singleton
public class InstalledPluginGatheringTask extends TaskSystem.Task {
private final ServerSensor<?> serverSensor;
private final ServerInfo serverInfo;
private final DBSystem dbSystem;
@Inject
public InstalledPluginGatheringTask(ServerSensor<?> serverSensor, ServerInfo serverInfo, DBSystem dbSystem) {
this.serverSensor = serverSensor;
this.serverInfo = serverInfo;
this.dbSystem = dbSystem;
}
@Override
public void register(RunnableFactory runnableFactory) {
runnableFactory.create(this)
.runTaskLater(20, TimeUnit.SECONDS);
}
@Override
public void run() {
List<PluginMetadata> installedPlugins = serverSensor.getInstalledPlugins();
ServerUUID serverUUID = serverInfo.getServerUUID();
List<PluginMetadata> previouslyInstalledPlugins = dbSystem.getDatabase()
.query(PluginMetadataQueries.getInstalledPlugins(serverUUID));
List<PluginMetadata> newPlugins = new ArrayList<>();
List<PluginMetadata> updatedPlugins = new ArrayList<>();
Set<String> installedPluginNames = new HashSet<>();
for (PluginMetadata installedPlugin : installedPlugins) {
installedPluginNames.add(installedPlugin.getName());
Optional<PluginMetadata> match = previouslyInstalledPlugins.stream()
.filter(plugin -> plugin.getName().equals(installedPlugin.getName()))
.findFirst();
if (match.isEmpty()) {
// New plugins are installedPlugins missing from previous list
newPlugins.add(installedPlugin);
} else {
PluginMetadata previousVersion = match.get();
String installedVersion = StringUtils.truncate(installedPlugin.getVersion(), PluginVersionTable.MAX_VERSION_LENGTH);
if (!installedVersion.equals(previousVersion.getVersion())) {
// Updated plugins are plugins in the list with different version
updatedPlugins.add(installedPlugin);
}
}
}
// Removed plugins are previously installed plugins missing from installed list
List<PluginMetadata> removedPlugins = previouslyInstalledPlugins.stream()
.map(PluginMetadata::getName)
.filter(pluginName -> !installedPluginNames.contains(pluginName))
// Uninstalled plugin version is marked as null
.map(pluginName -> new PluginMetadata(pluginName, null))
.collect(Collectors.toList());
long enableTime = PlanSystem.getServerEnableTime();
List<PluginMetadata> pluginChangeList = new ArrayList<>();
pluginChangeList.addAll(newPlugins);
pluginChangeList.addAll(updatedPlugins);
pluginChangeList.addAll(removedPlugins);
dbSystem.getDatabase().executeTransaction(new StorePluginVersionsTransaction(enableTime, serverUUID, pluginChangeList));
}
}

View File

@ -22,6 +22,7 @@ import com.djrapitops.plan.settings.config.ConfigNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Optional;
import java.util.function.Predicate;
/**
* Represents a change made to the config structure.
@ -103,6 +104,8 @@ public interface ConfigChange {
public boolean hasBeenApplied(Config config) {
return config.getNode(oldPath)
.map(ConfigNode::getString)
.map(String::trim)
.filter(Predicate.not(String::isEmpty))
.isEmpty()
&& config.getNode(newPath).isPresent();
}

View File

@ -270,6 +270,13 @@ public enum HtmlLang implements Lang {
LABEL_SERVER_SELECTOR("html.label.serverSelector", "Server selector"),
LABEL_APPLY("html.label.apply", "Apply"),
LABEL_POSSIBLY_OFFLINE("html.label.serverPossiblyOffline", "Possibly offline"),
LABEL_PLUGINS_CURRENTLY_INSTALLED("html.label.currentlyInstalledPlugins", "Currently Installed Plugins"),
LABEL_PLUGINS_HISTORY("html.label.pluginHistory", "Plugin History"),
LABEL_PLUGINS_VERSION_HISTORY("html.label.pluginVersionHistory", "Plugin Version History"),
LABEL_VERSION("html.label.version", "Version"),
LABEL_MODIFIED("html.label.modified", "Modified"),
LABEL_INSTALLED("html.label.installed", "Installed"),
LABEL_UNINSTALLED("html.label.uninstalled", "Uninstalled"),
LABEL_TABLE_VISIBLE_COLUMNS("html.label.table.visibleColumns", "Visible columns"),
LABEL_TABLE_SHOW_N_OF_M("html.label.table.showNofM", "Showing {{n}} of {{m}} entries"),

View File

@ -0,0 +1,98 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.storage.database.queries.objects;
import com.djrapitops.plan.delivery.domain.PluginHistoryMetadata;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.storage.database.queries.Query;
import com.djrapitops.plan.storage.database.queries.QueryStatement;
import com.djrapitops.plan.storage.database.sql.tables.PluginVersionTable;
import com.djrapitops.plan.storage.database.sql.tables.ServerTable;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NotNull;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
/**
* Queries to the {@link com.djrapitops.plan.storage.database.sql.tables.PluginVersionTable} go here.
*
* @author AuroraLS3
*/
public class PluginMetadataQueries {
private PluginMetadataQueries() {
/* static method class */
}
public static Query<List<PluginMetadata>> getInstalledPlugins(ServerUUID serverUUID) {
@Language("SQL")
String sql = SELECT + "*" + FROM + PluginVersionTable.TABLE_NAME +
WHERE + PluginVersionTable.SERVER_ID + "=" + ServerTable.SELECT_SERVER_ID +
ORDER_BY + PluginVersionTable.MODIFIED + " DESC";
return new QueryStatement<>(sql, 100) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, serverUUID.toString());
}
@Override
public List<PluginMetadata> processResults(ResultSet set) throws SQLException {
Set<String> foundPlugins = new HashSet<>();
List<PluginMetadata> installedPlugins = new ArrayList<>();
while (set.next()) {
String pluginName = set.getString(PluginVersionTable.PLUGIN_NAME);
// Only keep the latest information
if (foundPlugins.contains(pluginName)) continue;
foundPlugins.add(pluginName);
String version = set.getString(PluginVersionTable.VERSION);
if (!set.wasNull()) { // If version is null the plugin is marked as uninstalled.
installedPlugins.add(new PluginMetadata(pluginName, version));
}
}
return installedPlugins;
}
};
}
public static Query<List<PluginHistoryMetadata>> getPluginHistory(ServerUUID serverUUID) {
@Language("SQL")
String sql = SELECT + "*" + FROM + PluginVersionTable.TABLE_NAME +
WHERE + PluginVersionTable.SERVER_ID + "=" + ServerTable.SELECT_SERVER_ID +
ORDER_BY + PluginVersionTable.MODIFIED + " DESC, " + PluginVersionTable.PLUGIN_NAME;
return db -> db.queryList(sql, PluginMetadataQueries::extractHistoryMetadata, serverUUID);
}
@NotNull
private static PluginHistoryMetadata extractHistoryMetadata(ResultSet row) throws SQLException {
String name = row.getString(PluginVersionTable.PLUGIN_NAME);
String version = row.getString(PluginVersionTable.VERSION);
if (row.wasNull()) version = null;
long modified = row.getLong(PluginVersionTable.MODIFIED);
return new PluginHistoryMetadata(name, version, modified);
}
}

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.storage.database.sql.tables;
import com.djrapitops.plan.storage.database.DBType;
import com.djrapitops.plan.storage.database.sql.building.CreateTableBuilder;
import com.djrapitops.plan.storage.database.sql.building.Sql;
/**
* Represents plan_plugin_versions table.
* <p>
* Keeps track of plugin version history.
*
* @author AuroraLS3
*/
public class PluginVersionTable {
public static final String TABLE_NAME = "plan_plugin_versions";
public static final String ID = "id";
public static final String SERVER_ID = "server_id";
public static final String PLUGIN_NAME = "plugin_name";
public static final String VERSION = "version";
public static final String MODIFIED = "modified";
public static final int MAX_NAME_LENGTH = 100;
public static final int MAX_VERSION_LENGTH = 255;
public static final String INSERT_STATEMENT = "INSERT INTO " + TABLE_NAME + " ("
+ SERVER_ID + ','
+ PLUGIN_NAME + ','
+ VERSION + ','
+ MODIFIED
+ ") VALUES (" + ServerTable.SELECT_SERVER_ID + ", ?, ?, ?)";
private PluginVersionTable() {
/* Static information class */
}
public static String createTableSQL(DBType dbType) {
return CreateTableBuilder.create(TABLE_NAME, dbType)
.column(ID, Sql.INT).primaryKey()
.column(SERVER_ID, Sql.INT).notNull()
.column(PLUGIN_NAME, Sql.varchar(100)).notNull()
.column(VERSION, Sql.varchar(255))
.column(MODIFIED, Sql.LONG).notNull().defaultValue("0")
.foreignKey(SERVER_ID, ServerTable.TABLE_NAME, ServerTable.ID)
.toString();
}
}

View File

@ -0,0 +1,72 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.storage.database.transactions.events;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.storage.database.sql.tables.PluginVersionTable;
import com.djrapitops.plan.storage.database.transactions.ExecBatchStatement;
import com.djrapitops.plan.storage.database.transactions.Transaction;
import org.apache.commons.lang3.StringUtils;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
/**
* Stores changes to the plugin list found during enable.
*
* @author AuroraLS3
*/
public class StorePluginVersionsTransaction extends Transaction {
private final long time;
private final ServerUUID serverUUID;
private final List<PluginMetadata> changeList;
public StorePluginVersionsTransaction(long time, ServerUUID serverUUID, List<PluginMetadata> changeList) {
this.time = time;
this.serverUUID = serverUUID;
this.changeList = changeList;
}
@Override
protected void performOperations() {
execute(new ExecBatchStatement(PluginVersionTable.INSERT_STATEMENT) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
for (PluginMetadata plugin : changeList) {
statement.setString(1, serverUUID.toString());
statement.setString(2, StringUtils.truncate(plugin.getName(), PluginVersionTable.MAX_NAME_LENGTH));
if (plugin.getVersion() == null) {
statement.setNull(3, Types.VARCHAR);
} else {
statement.setString(3, StringUtils.truncate(plugin.getVersion(), PluginVersionTable.MAX_VERSION_LENGTH));
}
statement.setLong(4, time);
statement.addBatch();
}
}
});
}
// Visible for testing
public List<PluginMetadata> getChangeList() {
return changeList;
}
}

View File

@ -56,6 +56,7 @@ public class CreateTablesTransaction extends OperationCriticalTransaction {
// Ensure plan_security has id column
executeOther(new SecurityTableIdPatch());
execute(WebUserPreferencesTable.createTableSQL(dbType));
execute(PluginVersionTable.createTableSQL(dbType));
// DataExtension tables
execute(ExtensionIconTable.createTableSQL(dbType));

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU 使用率"
currentPlayerbase: "当前玩家数"
currentUptime: "正常运行时间"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "按天查看"
dayOfweek: "星期"
deadliestWeapon: "最致命的 PVP 武器"
@ -418,6 +419,7 @@ html:
information: "信息"
insights: "洞察"
insights30days: "30 天分析"
installed: "Installed"
irregular: "偶尔上线"
joinAddress: "加入地址"
joinAddresses: "加入地址"
@ -469,6 +471,7 @@ html:
mobDeaths: "被生物击杀数"
mobKdr: "生物 KDR"
mobKills: "生物击杀数"
modified: "Modified"
mostActiveGamemode: "最常玩的游戏模式"
mostPlayedWorld: "玩的最多的世界"
name: "名称"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "在线玩家(当前)"
playersOnlineOverview: "在线活动总览"
playtime: "游玩时间"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "插件"
pluginsOverview: "插件总览"
punchcard: "打卡"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "趋势"
trends30days: "30 天趋势"
uninstalled: "Uninstalled"
uniquePlayers: "独立玩家"
uniquePlayers7days: "独立玩家7天"
unit:
percentage: "百分比"
playerCount: "玩家数量"
users: "管理用户"
version: "Version"
veryActive: "非常活跃"
weekComparison: "每周对比"
weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU Usage"
currentPlayerbase: "Aktuální základna hráčů"
currentUptime: "Aktuální doba zapnutí"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Den po dni"
dayOfweek: "Den týdne"
deadliestWeapon: "Nejsmrtelnější PvP zbraň"
@ -418,6 +419,7 @@ html:
information: "INFORMACE"
insights: "Postřehy"
insights30days: "Postřehy za 30 dní"
installed: "Installed"
irregular: "Nepravidelný"
joinAddress: "Adresa Připojení"
joinAddresses: "IP Připojení"
@ -469,6 +471,7 @@ html:
mobDeaths: "Smrti způsobené moby"
mobKdr: "Mob KDR"
mobKills: "Zabití mobové"
modified: "Modified"
mostActiveGamemode: "Nejvíce aktivní mód"
mostPlayedWorld: "Nejvíc hraný svět"
name: "Jméno"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Hráči online (Nyní)"
playersOnlineOverview: "Přehled online aktivity"
playtime: "Herní čas"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Pluginy"
pluginsOverview: "Přehled pluginů"
punchcard: "Štítky"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Trend"
trends30days: "Trendy za 30 dní"
uninstalled: "Uninstalled"
uniquePlayers: "Unikátní hráči"
uniquePlayers7days: "Unikátních hráčů (7 dní)"
unit:
percentage: "Procento"
playerCount: "Počet hráčů"
users: "Manage Users"
version: "Version"
veryActive: "Velmi aktivní"
weekComparison: "Týdenní srovnání"
weekdays: "'Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek', 'Sobota', 'Neděle'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU Usage"
currentPlayerbase: "Aktuelle Spielerbasis"
currentUptime: "Current Uptime"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Tag für Tag"
dayOfweek: "Tag der Woche"
deadliestWeapon: "Tödlichste PvP Waffe"
@ -418,6 +419,7 @@ html:
information: "INFORMATION"
insights: "Insights"
insights30days: "Insights for 30 days"
installed: "Installed"
irregular: "Unregelmäßig"
joinAddress: "Join Address"
joinAddresses: "Join Addresses"
@ -469,6 +471,7 @@ html:
mobDeaths: "Tode durch Mobs"
mobKdr: "Mob KDR"
mobKills: "Mob Kills"
modified: "Modified"
mostActiveGamemode: "Meist genutzter Spielmodus"
mostPlayedWorld: "Meist gespielte Welt"
name: "Name"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Online Aktivitätsübersicht"
playtime: "Spielzeit"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Plugins"
pluginsOverview: "Plugins Overview"
punchcard: "Lochkarte"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Trend"
trends30days: "Trends für 30 Tage"
uninstalled: "Uninstalled"
uniquePlayers: "Einzigartige Spieler"
uniquePlayers7days: "Unique Players (7 days)"
unit:
percentage: "Percentage"
playerCount: "Player Count"
users: "Manage Users"
version: "Version"
veryActive: "Sehr aktiv"
weekComparison: "Wochenvergleich"
weekdays: "'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU Usage"
currentPlayerbase: "Current Playerbase"
currentUptime: "Current Uptime"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Day by Day"
dayOfweek: "Day of the Week"
deadliestWeapon: "Deadliest PvP Weapon"
@ -418,6 +419,7 @@ html:
information: "INFORMATION"
insights: "Insights"
insights30days: "Insights for 30 days"
installed: "Installed"
irregular: "Irregular"
joinAddress: "Join Address"
joinAddresses: "Join Addresses"
@ -469,6 +471,7 @@ html:
mobDeaths: "Mob caused Deaths"
mobKdr: "Mob KDR"
mobKills: "Mob Kills"
modified: "Modified"
mostActiveGamemode: "Most Active Gamemode"
mostPlayedWorld: "Most played World"
name: "Name"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Online Activity Overview"
playtime: "Playtime"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Plugins"
pluginsOverview: "Plugins Overview"
punchcard: "Punchcard"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Trend"
trends30days: "Trends for 30 days"
uninstalled: "Uninstalled"
uniquePlayers: "Unique Players"
uniquePlayers7days: "Unique Players (7 days)"
unit:
percentage: "Percentage"
playerCount: "Player Count"
users: "Manage Users"
version: "Version"
veryActive: "Very Active"
weekComparison: "Week Comparison"
weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU Usage"
currentPlayerbase: "base del jugador actual"
currentUptime: "Current Uptime"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Día a día"
dayOfweek: "Dia de la semana"
deadliestWeapon: "Arma PvP más mortal"
@ -418,6 +419,7 @@ html:
information: "INFORMACIÓN"
insights: "Insights"
insights30days: "Ideas por 30 días"
installed: "Installed"
irregular: "Irregular"
joinAddress: "Join Address"
joinAddresses: "Direcciones de entrada"
@ -469,6 +471,7 @@ html:
mobDeaths: "Muertes causadas por mobs"
mobKdr: "KDR de mobs"
mobKills: "Asesinatos de mobs"
modified: "Modified"
mostActiveGamemode: "Modo de juego mas activo"
mostPlayedWorld: "Mundo más jugado"
name: "Nombre"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Vista general de la actividad online"
playtime: "Tiempo de juego"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Plugins"
pluginsOverview: "Plugins Overview"
punchcard: "Tarjeta"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Tendencia"
trends30days: "Tendencias de 30 días"
uninstalled: "Uninstalled"
uniquePlayers: "Jugadores únicos"
uniquePlayers7days: "Unique Players (7 days)"
unit:
percentage: "Percentage"
playerCount: "Player Count"
users: "Manage Users"
version: "Version"
veryActive: "Muy activo"
weekComparison: "Comparación semanal"
weekdays: "'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "Suorittimen käyttö"
currentPlayerbase: "Nykyiset pelaajat"
currentUptime: "Käynnissäoloaika"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Päivittäinen katsaus"
dayOfweek: "Viikonpäivä"
deadliestWeapon: "Tappavin PvP Ase"
@ -418,6 +419,7 @@ html:
information: "TIETOJA"
insights: "Katsaukset"
insights30days: "Katsauksia 30 päivälle"
installed: "Installed"
irregular: "Epäsäännöllinen"
joinAddress: "Liittymisosoite"
joinAddresses: "Liittymisosoitteet"
@ -469,6 +471,7 @@ html:
mobDeaths: "Otusten aiheuttamat Kuolemat"
mobKdr: "Otus-Tapposuhde"
mobKills: "Tapetut Otukset"
modified: "Modified"
mostActiveGamemode: "Aktiivisin pelitila"
mostPlayedWorld: "Eniten pelattu maailma"
name: "Nimi"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Pelaajia paikalla (Nyt)"
playersOnlineOverview: "Yhteenveto Paikallaolosta"
playtime: "Peliaika"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Lisäosat"
pluginsOverview: "Lisäosien Yhteenveto"
punchcard: "Reikäkortti"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Suunta"
trends30days: "Suunnat 30 päivälle"
uninstalled: "Uninstalled"
uniquePlayers: "Uniikkeja pelaajia"
uniquePlayers7days: "Uniikkeja pelaajia (7 päivää)"
unit:
percentage: "Prosentti"
playerCount: "Pelaajamäärä"
users: "Manage Users"
version: "Version"
veryActive: "Todella Aktiivinen"
weekComparison: "Viikkojen vertaus"
weekdays: "'Maanantai', 'Tiistai', 'Keskiviikko', 'Torstai', 'Perjantai', 'Lauantai', 'Sunnuntai'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU Usage"
currentPlayerbase: "Base de Joueurs actuelle"
currentUptime: "Current Uptime"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Jour par Jour"
dayOfweek: "Jour de la Semaine"
deadliestWeapon: "1ère Arme de Combat (la plus mortelle)"
@ -418,6 +419,7 @@ html:
information: "INFORMATIONS"
insights: "Insights"
insights30days: "Perspectives sur 30 jours"
installed: "Installed"
irregular: "Irrégulier"
joinAddress: "Join Address"
joinAddresses: "Adresses de Connexion"
@ -469,6 +471,7 @@ html:
mobDeaths: "Morts causées par un Mob"
mobKdr: "Ratio - Kills / Morts de Mobs -"
mobKills: "Kills de Mobs"
modified: "Modified"
mostActiveGamemode: "Mode de Jeu le plus utilisé"
mostPlayedWorld: "Monde le plus Fréquenté"
name: "Nom"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Aperçu de l'Activité en Ligne"
playtime: "Temps de Jeu"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Plugins"
pluginsOverview: "Plugins Overview"
punchcard: "Carte Perforée"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Tendances"
trends30days: "Tendances sur 30 Jours"
uninstalled: "Uninstalled"
uniquePlayers: "Joueurs Uniques"
uniquePlayers7days: "Unique Players (7 days)"
unit:
percentage: "Percentage"
playerCount: "Player Count"
users: "Manage Users"
version: "Version"
veryActive: "Très Actif"
weekComparison: "Comparaison Hebdomadaire"
weekdays: "'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU Usage"
currentPlayerbase: "Playerbase Corrente"
currentUptime: "Current Uptime"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Giorno Per Giorno"
dayOfweek: "Giorno della Settimana"
deadliestWeapon: "Arma PvP Preferita"
@ -418,6 +419,7 @@ html:
information: "INFORMAZIONE"
insights: "Insights"
insights30days: "Grafico in 30 giorni"
installed: "Installed"
irregular: "Irregolare"
joinAddress: "Join Address"
joinAddresses: "Join Addresses"
@ -469,6 +471,7 @@ html:
mobDeaths: "Mob che hanno causato la Morte"
mobKdr: "Mob KDR"
mobKills: "Mob uccisi"
modified: "Modified"
mostActiveGamemode: "La Gamemode più attiva"
mostPlayedWorld: "Mondo più giocato"
name: "Nome"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Panoramica delle attività online"
playtime: "Gioco"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Plugins"
pluginsOverview: "Plugins Overview"
punchcard: "Presenza Settimanale"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Tendenza"
trends30days: "Tendenza per 30 giorni"
uninstalled: "Uninstalled"
uniquePlayers: "Giocatori unici"
uniquePlayers7days: "Unique Players (7 days)"
unit:
percentage: "Percentage"
playerCount: "Player Count"
users: "Manage Users"
version: "Version"
veryActive: "Molto Attivo"
weekComparison: "Confronto settimanale"
weekdays: "'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU使用率"
currentPlayerbase: "ログインプレイヤー"
currentUptime: "現状のアップタイム"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "詳細情報"
dayOfweek: "曜日"
deadliestWeapon: "最もPvPで使用されている武器"
@ -418,6 +419,7 @@ html:
information: "インフォメーション"
insights: "Insights"
insights30days: "1ヶ月のパンチボード"
installed: "Installed"
irregular: "たまにログインしている"
joinAddress: "参加したサーバーのアドレス"
joinAddresses: "参加したサーバーのアドレス"
@ -469,6 +471,7 @@ html:
mobDeaths: "Mobによって殺された回数"
mobKdr: "Mobに対してのKDR"
mobKills: "Mobを殺した回数"
modified: "Modified"
mostActiveGamemode: "最も使用したゲームモード"
mostPlayedWorld: "よくプレイしているワールド"
name: "名前"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "オンラインのプレイヤー(今)"
playersOnlineOverview: "接続状況の概要"
playtime: "プレイ時間"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "プラグイン"
pluginsOverview: "プラグイン一覧"
punchcard: "パンチカード"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "増減"
trends30days: "1ヶ月間の増減"
uninstalled: "Uninstalled"
uniquePlayers: "接続したプレイヤーの総数"
uniquePlayers7days: "直近7日間のユニークプレイヤー数"
unit:
percentage: "パーセンテージ"
playerCount: "プレイヤー数"
users: "Manage Users"
version: "Version"
veryActive: "とてもログインしている"
weekComparison: "直近1週間での比較"
weekdays: "'月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU Usage"
currentPlayerbase: "현재 플레이어 베이스"
currentUptime: "Current Uptime"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Day by Day"
dayOfweek: "요일"
deadliestWeapon: "치명적인 PvP 무기"
@ -418,6 +419,7 @@ html:
information: "정보"
insights: "Insights"
insights30days: "30일 동안의 인사이트"
installed: "Installed"
irregular: "불규칙한"
joinAddress: "Join Address"
joinAddresses: "Join Addresses"
@ -469,6 +471,7 @@ html:
mobDeaths: "몬스터한테 죽은 횟수"
mobKdr: "몬스터 KDR"
mobKills: "몬스터 킬수"
modified: "Modified"
mostActiveGamemode: "가장 활동적인 게임모드"
mostPlayedWorld: "가장 많이 플레이 한 맵"
name: "이름"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "온라인 활동 개요"
playtime: "플레이타임"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "플러그인"
pluginsOverview: "Plugins Overview"
punchcard: "펀치 카드"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "트렌드"
trends30days: "30일 동안의 트렌드"
uninstalled: "Uninstalled"
uniquePlayers: "기존 플레이어"
uniquePlayers7days: "Unique Players (7 days)"
unit:
percentage: "Percentage"
playerCount: "Player Count"
users: "Manage Users"
version: "Version"
veryActive: "매우 활성화된"
weekComparison: "주 비교"
weekdays: "'월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU Usage"
currentPlayerbase: "Huidige spelerbasis"
currentUptime: "Current Uptime"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Dag voor dag"
dayOfweek: "Dag van de Week"
deadliestWeapon: "Dodelijkste PvP-wapen"
@ -418,6 +419,7 @@ html:
information: "INFORMATIE"
insights: "Insights"
insights30days: "Inzichten voor 30 dagen"
installed: "Installed"
irregular: "Onregelmatig"
joinAddress: "Join Address"
joinAddresses: "Inlog adressen"
@ -469,6 +471,7 @@ html:
mobDeaths: "Mob veroorzaakt doden"
mobKdr: "Mob KDR"
mobKills: "Mob Moorden"
modified: "Modified"
mostActiveGamemode: "Meest actieve spelmodus"
mostPlayedWorld: "Meest gespeelde wereld"
name: "Naam"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Overzicht van online activiteiten"
playtime: "Speeltijd"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Plugins"
pluginsOverview: "Plugins Overview"
punchcard: "Ponskaart"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Trend"
trends30days: "Trends voor 30 dagen"
uninstalled: "Uninstalled"
uniquePlayers: "Unieke spelers"
uniquePlayers7days: "Unique Players (7 days)"
unit:
percentage: "Percentage"
playerCount: "Player Count"
users: "Manage Users"
version: "Version"
veryActive: "Heel Actief"
weekComparison: "Weekvergelijking"
weekdays: "'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag', 'Zondag'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU Usage"
currentPlayerbase: "Base de Jogadores Atual"
currentUptime: "Current Uptime"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Day by Day"
dayOfweek: "Day of the Week"
deadliestWeapon: "Deadliest PvP Weapon"
@ -418,6 +419,7 @@ html:
information: "INFORMATION"
insights: "Insights"
insights30days: "Insights for 30 days"
installed: "Installed"
irregular: "Irregular"
joinAddress: "Join Address"
joinAddresses: "Join Addresses"
@ -469,6 +471,7 @@ html:
mobDeaths: "Mortes causadas por Mobs"
mobKdr: "KDR por Mob"
mobKills: "Assassinato de Mobs"
modified: "Modified"
mostActiveGamemode: "Most Active Gamemode"
mostPlayedWorld: "Most played World"
name: "Nome"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Online Activity Overview"
playtime: "Tempo de Jogo"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Plugins"
pluginsOverview: "Plugins Overview"
punchcard: "Punchcard"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Trend"
trends30days: "Trends for 30 days"
uninstalled: "Uninstalled"
uniquePlayers: "Jogadores Únicos"
uniquePlayers7days: "Unique Players (7 days)"
unit:
percentage: "Percentage"
playerCount: "Player Count"
users: "Manage Users"
version: "Version"
veryActive: "Muito Ativo"
weekComparison: "Week Comparison"
weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "Использование ЦП"
currentPlayerbase: "Текущая база игроков"
currentUptime: "Время безотказной работы"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Статистика по дням"
dayOfweek: "День недели"
deadliestWeapon: "Самое смертоносное оружие в PvP"
@ -418,6 +419,7 @@ html:
information: "ИНФОРМАЦИЯ"
insights: "Инсайты"
insights30days: "Статистика за 30 дней"
installed: "Installed"
irregular: "Нерегулярный"
joinAddress: "Адрес входа"
joinAddresses: "Адресы Входа"
@ -469,6 +471,7 @@ html:
mobDeaths: "Смерть из-за мобов"
mobKdr: "Моб KDR"
mobKills: "Убийства мобов"
modified: "Modified"
mostActiveGamemode: "Самый активный игровой режим"
mostPlayedWorld: "Самый популярный мир"
name: "Имя"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Обзор сетевой активности"
playtime: "Время игры"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Плагины"
pluginsOverview: "Обзор плагинов"
punchcard: "Перфокарты"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Тенденция"
trends30days: "тенденция за 30 дней"
uninstalled: "Uninstalled"
uniquePlayers: "Уникальные игроки"
uniquePlayers7days: "Unique Players (7 days)"
unit:
percentage: "Percentage"
playerCount: "Player Count"
users: "Manage Users"
version: "Version"
veryActive: "Очень активный"
weekComparison: "Сравнение за неделю"
weekdays: "'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU Usage"
currentPlayerbase: "Şuanki Oyuncu Tabanı"
currentUptime: "Current Uptime"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Gün gün"
dayOfweek: "Day of the Week"
deadliestWeapon: "En Ölümcül PvP Silahı"
@ -418,6 +419,7 @@ html:
information: "DANIŞMA"
insights: "Insights"
insights30days: "30 günlük bilgiler"
installed: "Installed"
irregular: "Düzensiz"
joinAddress: "Join Address"
joinAddresses: "Adreslere Katıl"
@ -469,6 +471,7 @@ html:
mobDeaths: "Yaratık Yüzünden ölümler"
mobKdr: "Mob İstatistiği"
mobKills: "Öldürülen Mob"
modified: "Modified"
mostActiveGamemode: "En Aktif Oyun Modu"
mostPlayedWorld: "En çok oynanan Dünya"
name: "İsim"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Çevrimiçi Etkinliğe Genel Bakış"
playtime: "Oyun Süresi"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Pluginler"
pluginsOverview: "Plugins Overview"
punchcard: "Punchcard"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Trend"
trends30days: "30 günlük trendler"
uninstalled: "Uninstalled"
uniquePlayers: "Sunucuya İlk Defa Girenler"
uniquePlayers7days: "Unique Players (7 days)"
unit:
percentage: "Percentage"
playerCount: "Player Count"
users: "Manage Users"
version: "Version"
veryActive: "Çok Aktif"
weekComparison: "Hafta Karşılaştırması"
weekdays: "'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "Використання ЦП"
currentPlayerbase: "Поточна база гравців"
currentUptime: "Час безвідмовної роботи"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "Статистика за днями"
dayOfweek: "День тижня"
deadliestWeapon: "Найсмертоносніша зброя в PvP"
@ -418,6 +419,7 @@ html:
information: "Інформація"
insights: "Інсайти"
insights30days: "Статистика за 30 днів"
installed: "Installed"
irregular: "Нерегулярний"
joinAddress: "Адреса входу"
joinAddresses: "Адреси входу"
@ -469,6 +471,7 @@ html:
mobDeaths: "Смерть через мобів"
mobKdr: "Моб KDR"
mobKills: "Вбивства мобів"
modified: "Modified"
mostActiveGamemode: "Найактивніший ігровий режим"
mostPlayedWorld: "Найпопулярніший світ"
name: "Ім'я"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "Гравці онлайн (зараз)"
playersOnlineOverview: "Огляд мережевої активності"
playtime: "Час гри"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "Плагіни"
pluginsOverview: "Огляд плагінів"
punchcard: "Перфокарти"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "Тенденція"
trends30days: "Тенденція за 30 днів"
uninstalled: "Uninstalled"
uniquePlayers: "Унікальні гравці"
uniquePlayers7days: "Унікальні гравці (7 днів)"
unit:
percentage: "Відсоток"
playerCount: "Кількість гравців"
users: "Manage Users"
version: "Version"
veryActive: "Дуже активний"
weekComparison: "Порівняння за тиждень"
weekdays: "'Понеділок', 'Вівторок', 'Середа', 'Четвер', 'П`ятниця', 'Субота', 'Неділя'"

View File

@ -325,6 +325,7 @@ html:
cpuUsage: "CPU 使用率"
currentPlayerbase: "目前玩家數量"
currentUptime: "Current Uptime"
currentlyInstalledPlugins: "Currently Installed Plugins"
dayByDay: "按天查看"
dayOfweek: "星期"
deadliestWeapon: "最致命的 PvP 武器"
@ -418,6 +419,7 @@ html:
information: "訊息"
insights: "Insights"
insights30days: "30 天分析"
installed: "Installed"
irregular: "偶爾上線"
joinAddress: "Join Address"
joinAddresses: "加入位址"
@ -469,6 +471,7 @@ html:
mobDeaths: "被生物擊殺數"
mobKdr: "生物 KDR"
mobKills: "生物擊殺數"
modified: "Modified"
mostActiveGamemode: "最常玩的遊戲模式"
mostPlayedWorld: "玩的最多的世界"
name: "名稱"
@ -512,6 +515,8 @@ html:
playersOnlineNow: "線上玩家 (Now)"
playersOnlineOverview: "線上活動概覽"
playtime: "遊玩時間"
pluginHistory: "Plugin History"
pluginVersionHistory: "Plugin Version History"
plugins: "插件"
pluginsOverview: "插件概覽"
punchcard: "打卡"
@ -599,12 +604,14 @@ html:
tps: "TPS"
trend: "趨勢"
trends30days: "30 天趨勢"
uninstalled: "Uninstalled"
uniquePlayers: "獨立玩家"
uniquePlayers7days: "獨立玩家 (7 days)"
unit:
percentage: "Percentage"
playerCount: "Player Count"
users: "Manage Users"
version: "Version"
veryActive: "非常活躍"
weekComparison: "每週對比"
weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'"

View File

@ -155,7 +155,9 @@ class AccessControlTest {
Arguments.of("/v1/deleteGroup?group=admin&moveTo=no_access", WebPermission.MANAGE_GROUPS, 400, 403),
Arguments.of("/v1/saveGroupPermissions?group=admin", WebPermission.MANAGE_GROUPS, 400, 403),
Arguments.of("/v1/preferences", WebPermission.ACCESS, 200, 200),
Arguments.of("/v1/storePreferences", WebPermission.ACCESS, 400, 400)
Arguments.of("/v1/storePreferences", WebPermission.ACCESS, 400, 400),
Arguments.of("/v1/pluginHistory?server=" + TestConstants.SERVER_UUID_STRING, WebPermission.PAGE_NETWORK_PLUGIN_HISTORY, 200, 403),
Arguments.of("/v1/pluginHistory?server=" + TestConstants.SERVER_UUID_STRING, WebPermission.PAGE_SERVER_PLUGIN_HISTORY, 200, 403)
);
}

View File

@ -146,6 +146,7 @@ class AccessControlVisibilityTest {
Arguments.arguments(WebPermission.PAGE_SERVER_PERFORMANCE_GRAPHS, "performance-graphs", "performance"),
Arguments.arguments(WebPermission.PAGE_SERVER_PERFORMANCE_OVERVIEW, "performance-as-numbers", "performance"),
Arguments.arguments(WebPermission.PAGE_SERVER_PERFORMANCE_OVERVIEW, "performance-insights", "performance"),
Arguments.arguments(WebPermission.PAGE_SERVER_PLUGIN_HISTORY, "server-plugin-history", "plugin-history"),
Arguments.arguments(WebPermission.PAGE_SERVER_PLUGINS, "server-plugin-data", "plugins-overview")
);
}
@ -174,6 +175,7 @@ class AccessControlVisibilityTest {
Arguments.arguments(WebPermission.PAGE_NETWORK_GEOLOCATIONS_MAP, "geolocations", "geolocations"),
Arguments.arguments(WebPermission.PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY, "ping-per-country", "geolocations"),
Arguments.arguments(WebPermission.PAGE_NETWORK_PERFORMANCE, "row-network-performance-0", "performance"),
Arguments.arguments(WebPermission.PAGE_NETWORK_PLUGIN_HISTORY, "network-plugin-history", "plugin-history"),
Arguments.arguments(WebPermission.PAGE_NETWORK_PLUGINS, "server-plugin-data", "plugins-overview")
);
}
@ -221,7 +223,7 @@ class AccessControlVisibilityTest {
}
@DisplayName("Whole page is not visible with permission")
@ParameterizedTest(name = "Access with no visibility (needs {0}) can't see element #{1} in /{2}")
@ParameterizedTest(name = "Access with no visibility needs {0} can't see element #{1} in /{2}")
@MethodSource("pageLevelVisibleCases")
void pageNotVisible(WebPermission permission, String element, String page, Database database, ChromeDriver driver) throws Exception {
User user = registerUser(database);
@ -272,7 +274,7 @@ class AccessControlVisibilityTest {
}
@DisplayName("Server element is not visible without permission")
@ParameterizedTest(name = "Access to server page with no visibility (needs {0}) can't see element #{1} in section /server/uuid/{2}")
@ParameterizedTest(name = "Access to server page with no visibility needs {0} can't see element #{1} in section /server/uuid/{2}")
@MethodSource("serverPageElementVisibleCases")
void serverPageElementNotVisible(WebPermission permission, String element, String section, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception {
User user = registerUser(database, WebPermission.ACCESS_SERVER);
@ -310,7 +312,7 @@ class AccessControlVisibilityTest {
}
@DisplayName("Network element is not visible without permission")
@ParameterizedTest(name = "Access to network page with no visibility (needs {0}) can't see element #{1} in section /network/{2}")
@ParameterizedTest(name = "Access to network page with no visibility needs {0} can't see element #{1} in section /network/{2}")
@MethodSource("networkPageElementVisibleCases")
void networkPageElementNotVisible(WebPermission permission, String element, String section, Database database, ChromeDriver driver) throws Exception {
User user = registerUser(database, WebPermission.ACCESS_NETWORK);
@ -343,7 +345,7 @@ class AccessControlVisibilityTest {
}
@DisplayName("Player element is not visible without permission")
@ParameterizedTest(name = "Access to player page with no visibility (needs {0}) can't see element #{1} in section /player/uuid/{2}")
@ParameterizedTest(name = "Access to player page with no visibility needs {0} can't see element #{1} in section /player/uuid/{2}")
@MethodSource("playerPageVisibleCases")
void playerPageElementNotVisible(WebPermission permission, String element, String section, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception {
User user = registerUser(database, WebPermission.ACCESS_PLAYER);

View File

@ -91,6 +91,7 @@ class HttpAccessControlTest {
"/v1/saveGroupPermissions",
"/v1/deleteGroup",
"/v1/storePreferences",
"/v1/pluginHistory?server=" + TestConstants.SERVER_UUID_STRING,
"/manage",
"/auth/register",
"/auth/login",

View File

@ -0,0 +1,132 @@
/*
* 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.gathering.timed;
import com.djrapitops.plan.gathering.ServerSensor;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.transactions.Transaction;
import com.djrapitops.plan.storage.database.transactions.events.StorePluginVersionsTransaction;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import utilities.TestConstants;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
/**
* Tests to ensure that plugin gathering works as intended.
*
* @author AuroraLS3
*/
@ExtendWith(MockitoExtension.class)
class InstalledPluginGatheringTaskTest {
@Mock
ServerSensor<?> serverSensor;
@Mock
ServerInfo serverInfo;
@Mock
DBSystem dbSystem;
@Mock
Database database;
InstalledPluginGatheringTask underTest;
Transaction capturedTransaction;
@BeforeEach
void setup() {
when(database.executeTransaction(any())).then(invocation -> {
capturedTransaction = invocation.getArgument(0);
return CompletableFuture.allOf();
});
when(dbSystem.getDatabase()).thenReturn(database);
when(serverInfo.getServerUUID()).thenReturn(TestConstants.SERVER_UUID);
underTest = new InstalledPluginGatheringTask(serverSensor, serverInfo, dbSystem);
}
@Test
void newPluginsAreIncluded() {
List<PluginMetadata> previouslyInstalledPlugins = List.of();
List<PluginMetadata> installedPlugins = List.of(
new PluginMetadata("Plan", "5.6 build 2121"),
new PluginMetadata("LittleChef", "1.0.2")
);
when(database.query(any())).thenReturn(previouslyInstalledPlugins);
when(serverSensor.getInstalledPlugins()).thenReturn(installedPlugins);
underTest.run();
List<PluginMetadata> changeList = ((StorePluginVersionsTransaction) capturedTransaction).getChangeList();
assertEquals(installedPlugins, changeList);
}
@Test
void onlyUpdatedPluginsAreIncluded() {
List<PluginMetadata> previouslyInstalledPlugins = List.of(
new PluginMetadata("Plan", "5.6 build 2121"),
new PluginMetadata("LittleChef", "1.0.2")
);
List<PluginMetadata> installedPlugins = List.of(
new PluginMetadata("Plan", "5.6 build 2121"),
new PluginMetadata("LittleChef", "1.0.3")
);
when(database.query(any())).thenReturn(previouslyInstalledPlugins);
when(serverSensor.getInstalledPlugins()).thenReturn(installedPlugins);
underTest.run();
List<PluginMetadata> expected = List.of(
new PluginMetadata("LittleChef", "1.0.3")
);
List<PluginMetadata> changeList = ((StorePluginVersionsTransaction) capturedTransaction).getChangeList();
assertEquals(expected, changeList);
}
@Test
void removedPluginsAreIncluded() {
List<PluginMetadata> previouslyInstalledPlugins = List.of(
new PluginMetadata("Plan", "5.6 build 2121"),
new PluginMetadata("LittleChef", "1.0.2")
);
List<PluginMetadata> installedPlugins = List.of(
new PluginMetadata("Plan", "5.6 build 2121")
);
when(database.query(any())).thenReturn(previouslyInstalledPlugins);
when(serverSensor.getInstalledPlugins()).thenReturn(installedPlugins);
underTest.run();
List<PluginMetadata> expected = List.of(
new PluginMetadata("LittleChef", null)
);
List<PluginMetadata> changeList = ((StorePluginVersionsTransaction) capturedTransaction).getChangeList();
assertEquals(expected, changeList);
}
}

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionQue
import com.djrapitops.plan.storage.database.queries.*;
import com.djrapitops.plan.storage.database.queries.analysis.PlayerRetentionQueriesTest;
import com.djrapitops.plan.storage.database.queries.analysis.TopListQueriesTest;
import com.djrapitops.plan.storage.database.queries.objects.PluginMetadataQueriesTest;
import com.djrapitops.plan.storage.database.transactions.commands.ChangeUserUUIDTransactionTest;
import com.djrapitops.plan.storage.database.transactions.commands.CombineUserTransactionTest;
import com.djrapitops.plan.storage.database.transactions.patches.AfterBadJoinAddressDataCorrectionPatchTest;
@ -45,6 +46,7 @@ public interface DatabaseTestAggregate extends
ExtensionQueryResultTableDataQueryTest,
BadJoinAddressDataCorrectionPatchTest,
AfterBadJoinAddressDataCorrectionPatchTest,
PlayerRetentionQueriesTest {
PlayerRetentionQueriesTest,
PluginMetadataQueriesTest {
/* Collects all query tests together so its easier to implement database tests */
}

View File

@ -0,0 +1,54 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.storage.database.queries.objects;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import com.djrapitops.plan.storage.database.DatabaseTestPreparer;
import com.djrapitops.plan.storage.database.transactions.events.StorePluginVersionsTransaction;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Tests for {@link PluginMetadataQueries}.
*
* @author AuroraLS3
*/
public interface PluginMetadataQueriesTest extends DatabaseTestPreparer {
@Test
@DisplayName("Plugin Metadata is stored")
default void pluginMetadataIsStored() {
List<PluginMetadata> changeSet = List.of(
new PluginMetadata("Plan", "5.6 build 2121"),
new PluginMetadata("LittleChef", "1.0.2"),
new PluginMetadata("LittleFX", null)
);
db().executeTransaction(new StorePluginVersionsTransaction(System.currentTimeMillis(), serverUUID(), changeSet));
List<PluginMetadata> expected = List.of(
new PluginMetadata("Plan", "5.6 build 2121"),
new PluginMetadata("LittleChef", "1.0.2")
);
List<PluginMetadata> result = db().query(PluginMetadataQueries.getInstalledPlugins(serverUUID()));
assertEquals(expected, result);
}
}

View File

@ -17,6 +17,9 @@
package net.playeranalytics.plan.gathering;
import com.djrapitops.plan.gathering.ServerSensor;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import net.fabricmc.loader.api.ModContainer;
import net.fabricmc.loader.impl.FabricLoaderImpl;
import net.minecraft.entity.Entity;
import net.minecraft.server.dedicated.MinecraftDedicatedServer;
import net.minecraft.server.world.ServerWorld;
@ -25,6 +28,7 @@ import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Singleton
@ -89,4 +93,14 @@ public class FabricSensor implements ServerSensor<ServerWorld> {
public List<String> getOnlinePlayerNames() {
return Arrays.asList(server.getPlayerNames());
}
@Override
public List<PluginMetadata> getInstalledPlugins() {
return FabricLoaderImpl.INSTANCE.getMods().stream()
.map(ModContainer::getMetadata)
.map(metadata -> new PluginMetadata(
Optional.ofNullable(metadata.getName()).orElse(metadata.getId()),
metadata.getVersion().getFriendlyString()))
.toList();
}
}

View File

@ -24,6 +24,7 @@ import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
import com.djrapitops.plan.gathering.ShutdownHook;
import com.djrapitops.plan.gathering.timed.InstalledPluginGatheringTask;
import com.djrapitops.plan.gathering.timed.ServerTPSCounter;
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
import com.djrapitops.plan.settings.upkeep.ConfigStoreTask;
@ -98,4 +99,8 @@ public interface FabricTaskModule {
@Binds
@IntoSet
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
@Binds
@IntoSet
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
}

View File

@ -18,7 +18,9 @@ package com.djrapitops.plan.gathering;
import cn.nukkit.Player;
import cn.nukkit.level.Level;
import cn.nukkit.plugin.Plugin;
import com.djrapitops.plan.PlanNukkit;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -74,4 +76,13 @@ public class NukkitSensor implements ServerSensor<Level> {
.map(Player::getName)
.collect(Collectors.toList());
}
@Override
public List<PluginMetadata> getInstalledPlugins() {
return plugin.getServer().getPluginManager()
.getPlugins().values().stream()
.map(Plugin::getDescription)
.map(description -> new PluginMetadata(description.getName(), description.getVersion()))
.collect(Collectors.toList());
}
}

View File

@ -25,6 +25,7 @@ import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
import com.djrapitops.plan.gathering.ShutdownHook;
import com.djrapitops.plan.gathering.timed.InstalledPluginGatheringTask;
import com.djrapitops.plan.gathering.timed.NukkitPingCounter;
import com.djrapitops.plan.gathering.timed.ServerTPSCounter;
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
@ -98,4 +99,8 @@ public interface NukkitTaskModule {
@Binds
@IntoSet
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
@Binds
@IntoSet
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
}

View File

@ -36,6 +36,7 @@ const PlayerbaseOverview = React.lazy(() => import("./views/server/PlayerbaseOve
const ServerPlayers = React.lazy(() => import("./views/server/ServerPlayers"));
const ServerGeolocations = React.lazy(() => import("./views/server/ServerGeolocations"));
const ServerPerformance = React.lazy(() => import("./views/server/ServerPerformance"));
const ServerPluginHistory = React.lazy(() => import('./views/server/ServerPluginHistory'));
const ServerPluginData = React.lazy(() => import("./views/server/ServerPluginData"));
const ServerWidePluginData = React.lazy(() => import("./views/server/ServerWidePluginData"));
const ServerJoinAddresses = React.lazy(() => import("./views/server/ServerJoinAddresses"));
@ -50,6 +51,7 @@ const NetworkPlayerRetention = React.lazy(() => import("./views/network/NetworkP
const NetworkGeolocations = React.lazy(() => import("./views/network/NetworkGeolocations"));
const NetworkPlayerbaseOverview = React.lazy(() => import("./views/network/NetworkPlayerbaseOverview"));
const NetworkPerformance = React.lazy(() => import("./views/network/NetworkPerformance"));
const NetworkPluginHistory = React.lazy(() => import('./views/network/NetworkPluginHistory'));
const PlayersPage = React.lazy(() => import("./views/layout/PlayersPage"));
const AllPlayers = React.lazy(() => import("./views/players/AllPlayers"));
@ -163,6 +165,7 @@ function App() {
<Route path="players" element={<Lazy><ServerPlayers/></Lazy>}/>
<Route path="geolocations" element={<Lazy><ServerGeolocations/></Lazy>}/>
<Route path="performance" element={<Lazy><ServerPerformance/></Lazy>}/>
<Route path="plugin-history" element={<Lazy><ServerPluginHistory/></Lazy>}/>
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
<Route path="*" element={<ErrorView error={{
@ -183,6 +186,7 @@ function App() {
<Route path="join-addresses" element={<Lazy><NetworkJoinAddresses/></Lazy>}/>
<Route path="players" element={<Lazy><AllPlayers/></Lazy>}/>
<Route path="geolocations" element={<Lazy><NetworkGeolocations/></Lazy>}/>
<Route path="plugin-history" element={<Lazy><NetworkPluginHistory/></Lazy>}/>
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
<Route path="*" element={<ErrorView error={{

View File

@ -2,11 +2,12 @@ import React, {useEffect, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
const TabButton = ({id, name, href, icon, color, active}) => {
const TabButton = ({id, name, href, icon, color, active, disabled}) => {
const navigate = useNavigate();
return (
<li className="nav-item" id={id}>
<button className={"nav-link col-black" + (active ? ' active' : '')} aria-selected={active} role="tab"
<li className={"nav-item" + (disabled ? ' disabled' : '')} id={id}>
<button disabled={disabled} className={"nav-link col-black" + (active ? ' active' : '')}
aria-selected={active} role="tab"
onClick={() => navigate('#' + href, {replace: true})}>
<Fa icon={icon} className={'col-' + color}/> {name}
</button>
@ -26,6 +27,7 @@ const TabButtons = ({tabs, selectedTab}) => {
icon={tab.icon}
color={tab.color}
active={tab.href === selectedTab}
disabled={tab.disabled}
/>
))}
</ul>

View File

@ -0,0 +1,68 @@
import React from 'react';
import CardHeader from "../CardHeader";
import {faCube, faCubes, faSignal} from "@fortawesome/free-solid-svg-icons";
import {Card} from "react-bootstrap";
import {ErrorViewCard} from "../../../views/ErrorView";
import FormattedDate from "../../text/FormattedDate";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faCalendar} from "@fortawesome/free-regular-svg-icons";
import {useTranslation} from "react-i18next";
import {CardLoader} from "../../navigation/Loader";
import DataTablesTable from "../../table/DataTablesTable";
const PluginCurrentCard = ({data, loadingError}) => {
const {t} = useTranslation();
if (loadingError) return <ErrorViewCard error={loadingError}/>;
if (!data) return <CardLoader/>;
const history = [];
for (const entry of data.history) {
if (history.find(e => e.name === entry.name)) continue;
history.push(entry);
}
const table = {
columns: [{
title: <><Fa icon={faCube}/> {t('html.label.name')}</>,
data: "name"
}, {
title: <><Fa icon={faSignal}/> {t('html.label.version')}</>,
data: "version"
}, {
title: <><Fa icon={faCalendar}/> {t('html.label.modified')}</>,
data: {_: "modified", display: "modifiedDisplay"}
}],
data: history.length ? history.filter(entry => entry.version)
.map(entry => {
return {
name: entry.name,
version: t(entry.version),
modified: entry.modified,
modifiedDisplay: <FormattedDate date={entry.modified}/>
}
}) : [{name: t('generic.noData'), version: '', 'modified': 0, modifiedDisplay: ''}]
};
const options = {
responsive: true,
deferRender: true,
columns: table.columns,
data: table.data,
pagingType: "numbers",
order: [[0, "desc"]]
}
const rowKeyFunction = (row, column) => {
return row.name + "-" + row.version + '-' + row.modified + '-' + JSON.stringify(column?.data);
}
return (
<Card>
<CardHeader icon={faCubes} label={'html.label.currentlyInstalledPlugins'} color={"indigo"}/>
<DataTablesTable id={"plugin-current"} options={options} rowKeyFunction={rowKeyFunction}/>
</Card>
)
};
export default PluginCurrentCard

View File

@ -0,0 +1,62 @@
import React from 'react';
import CardHeader from "../CardHeader";
import {faCodeCompare, faCube, faSignal} from "@fortawesome/free-solid-svg-icons";
import {Card} from "react-bootstrap";
import {ErrorViewCard} from "../../../views/ErrorView";
import FormattedDate from "../../text/FormattedDate";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faCalendar} from "@fortawesome/free-regular-svg-icons";
import {useTranslation} from "react-i18next";
import {CardLoader} from "../../navigation/Loader";
import DataTablesTable from "../../table/DataTablesTable";
const PluginHistoryCard = ({data, loadingError}) => {
const {t} = useTranslation();
if (loadingError) return <ErrorViewCard error={loadingError}/>;
if (!data) return <CardLoader/>;
const history = data.history;
const table = {
columns: [{
title: <><Fa icon={faCube}/> {t('html.label.name')}</>,
data: "name"
}, {
title: <><Fa icon={faSignal}/> {t('html.label.version')}</>,
data: "version"
}, {
title: <><Fa icon={faCalendar}/> {t('html.label.modified')}</>,
data: {_: "modified", display: "modifiedDisplay"}
}],
data: history.length ? history.map(entry => {
return {
name: entry.name,
version: t(entry.version || 'html.label.uninstalled'),
modified: entry.modified,
modifiedDisplay: <FormattedDate date={entry.modified}/>
}
}) : [{name: t('generic.noData'), version: '', 'modified': 0, modifiedDisplay: ''}]
};
const options = {
responsive: true,
deferRender: true,
columns: table.columns,
data: table.data,
pagingType: "numbers",
order: [[2, "desc"]]
}
const rowKeyFunction = (row, column) => {
return row.name + "-" + row.version + '-' + row.modified + '-' + JSON.stringify(column?.data);
}
return (
<Card>
<CardHeader icon={faCodeCompare} label={'html.label.pluginVersionHistory'} color={"indigo"}/>
<DataTablesTable id={"plugin-history"} options={options} rowKeyFunction={rowKeyFunction}/>
</Card>
)
};
export default PluginHistoryCard

View File

@ -153,13 +153,16 @@ const PerformanceGraphsCard = ({data}) => {
setPerformanceSeries(series);
}, [data, setPerformanceSeries])
const dataIncludesGameServers = useMemo(() => data.servers && Boolean(data.servers.filter(server => !server.proxy).length), [data]);
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}/>
element: <Tab data={performanceSeries.tps} yAxis={yAxisConfigurations.TPS}/>,
disabled: !dataIncludesGameServers
}, {
name: t('html.label.cpu'), icon: faTachometerAlt, color: 'amber', href: 'cpu',
element: <Tab data={performanceSeries.cpu} yAxis={yAxisConfigurations.CPU}/>
@ -168,10 +171,12 @@ const PerformanceGraphsCard = ({data}) => {
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}/>
element: <Tab data={performanceSeries.entities} yAxis={yAxisConfigurations.ENTITIES}/>,
disabled: !dataIncludesGameServers
}, {
name: t('html.label.loadedChunks'), icon: faMap, color: 'blue-grey', href: 'chunks',
element: <Tab data={performanceSeries.chunks} yAxis={yAxisConfigurations.CHUNKS}/>
element: <Tab data={performanceSeries.chunks} yAxis={yAxisConfigurations.CHUNKS}/>,
disabled: !dataIncludesGameServers
}, {
name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk',
element: <Tab data={performanceSeries.disk} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
@ -180,7 +185,7 @@ const PerformanceGraphsCard = ({data}) => {
element: networkMetadata ? <PingTab identifier={networkMetadata.currentServer.serverUUID}/> :
<ChartLoader/>
},
], [performanceSeries, networkMetadata, t]);
], [performanceSeries, networkMetadata, t, dataIncludesGameServers]);
if (!data || !Object.values(data).length) return <CardLoader/>
if (data.errors.length) {

View File

@ -1,6 +1,6 @@
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchOptimizedPerformance, fetchPingGraph} from "../../../../service/serverService";
import {fetchOptimizedPerformance, fetchPingGraph, fetchPluginHistory} from "../../../../service/serverService";
import {ErrorViewBody} from "../../../../views/ErrorView";
import {useTranslation} from "react-i18next";
import {Card} from "react-bootstrap";
@ -15,40 +15,46 @@ import WorldPerformanceGraph from "../../../graphs/performance/WorldPerformanceG
import DiskPerformanceGraph from "../../../graphs/performance/DiskPerformanceGraph";
import PingGraph from "../../../graphs/performance/PingGraph";
import {mapPerformanceDataToSeries} from "../../../../util/graphs";
import {useAuth} from "../../../../hooks/authenticationHook";
const AllGraphTab = ({data, dataSeries, loadingError}) => {
const AllGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
return <AllPerformanceGraph id="server-performance-all-chart" data={data} dataSeries={dataSeries}/>
return <AllPerformanceGraph id="server-performance-all-chart" data={data} dataSeries={dataSeries}
pluginHistorySeries={pluginHistorySeries}/>
}
const TpsGraphTab = ({data, dataSeries, loadingError}) => {
const TpsGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
return <TpsPerformanceGraph id="server-performance-tps-chart" data={data} dataSeries={dataSeries}/>
return <TpsPerformanceGraph id="server-performance-tps-chart" data={data} dataSeries={dataSeries}
pluginHistorySeries={pluginHistorySeries}/>
}
const CpuRamGraphTab = ({data, dataSeries, loadingError}) => {
const CpuRamGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
return <CpuRamPerformanceGraph id="server-performance-cpuram-chart" data={data} dataSeries={dataSeries}/>
return <CpuRamPerformanceGraph id="server-performance-cpuram-chart" data={data} dataSeries={dataSeries}
pluginHistorySeries={pluginHistorySeries}/>
}
const WorldGraphTab = ({data, dataSeries, loadingError}) => {
const WorldGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
return <WorldPerformanceGraph id="server-performance-world-chart" data={data} dataSeries={dataSeries}/>
return <WorldPerformanceGraph id="server-performance-world-chart" data={data} dataSeries={dataSeries}
pluginHistorySeries={pluginHistorySeries}/>
}
const DiskGraphTab = ({data, dataSeries, loadingError}) => {
const DiskGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
return <DiskPerformanceGraph id="server-performance-disk-chart" data={data} dataSeries={dataSeries}/>
return <DiskPerformanceGraph id="server-performance-disk-chart" data={data} dataSeries={dataSeries}
pluginHistorySeries={pluginHistorySeries}/>
}
const PingGraphTab = ({identifier}) => {
@ -61,37 +67,79 @@ const PingGraphTab = ({identifier}) => {
const PerformanceGraphsCard = () => {
const {t} = useTranslation();
const {authRequired, hasPermission} = useAuth();
const {identifier} = useParams();
const {data, loadingError} = useDataRequest(fetchOptimizedPerformance, [identifier]);
const [parsedData, setParsedData] = useState(undefined)
const [parsedData, setParsedData] = useState(undefined);
const {
data: pluginHistory,
loadingError: pluginHistoryLoadingError
} = useDataRequest(fetchPluginHistory, [identifier], authRequired && hasPermission('page.server.plugin.history'));
const [pluginHistorySeries, setPluginHistorySeries] = useState({});
useEffect(() => {
if (data) {
mapPerformanceDataToSeries(data.values).then(parsed => setParsedData(parsed))
}
}, [data, setParsedData]);
useEffect(() => {
// https://stackoverflow.com/a/34890276/20825073
const groupBy = function (xs, key) {
return xs.reduce(function (rv, x) {
(rv[x[key]] = rv[x[key]] || []).push(x);
return rv;
}, {});
};
if (pluginHistory) {
const grouped = groupBy(pluginHistory.history.reverse(), 'modified');
setPluginHistorySeries({
type: 'flags',
accessibility: {
exposeAsGroupOnly: true,
description: t('html.label.pluginVersionHistory')
},
tooltip: {headerFormat: ''},
data: Object.entries(grouped).map(entry => {
const installedLines = entry[1].filter(p => p.version).map(plugin => plugin.name + ': ' + plugin.version).join(', <br>');
const uninstalledLines = entry[1].filter(p => !p.version).map(plugin => plugin.name).join(', <br>');
return {
x: entry[0],
title: entry[1].length,
text: (installedLines.length ? '<b>' + t('html.label.installed') + '</b><br>' + installedLines : '') +
(uninstalledLines.length ? '<b>' + t('html.label.uninstalled') + '</b><br>' + uninstalledLines : '')
}
})
})
}
}, [pluginHistory, setPluginHistorySeries, t]);
return <Card id={"performance-graphs"}>
<CardTabs tabs={[
{
name: t('html.label.all'), icon: faGears, color: 'blue-grey', href: 'all',
element: <AllGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
element: <AllGraphTab data={data} dataSeries={parsedData} pluginHistorySeries={pluginHistorySeries}
loadingError={loadingError || pluginHistoryLoadingError}/>
}, {
name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps',
element: <TpsGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
element: <TpsGraphTab data={data} dataSeries={parsedData} pluginHistorySeries={pluginHistorySeries}
loadingError={loadingError || pluginHistoryLoadingError}/>
}, {
name: t('html.label.cpuRam'), icon: faMicrochip, color: 'light-green', href: 'cpu-ram',
element: <CpuRamGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
element: <CpuRamGraphTab data={data} dataSeries={parsedData} pluginHistorySeries={pluginHistorySeries}
loadingError={loadingError || pluginHistoryLoadingError}/>
}, {
name: t('html.label.world'), icon: faMap, color: 'purple', href: 'world-load',
element: <WorldGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
element: <WorldGraphTab data={data} dataSeries={parsedData} pluginHistorySeries={pluginHistorySeries}
loadingError={loadingError || pluginHistoryLoadingError}/>
}, {
name: t('html.label.ping'), icon: faSignal, color: 'amber', href: 'ping',
element: <PingGraphTab identifier={identifier}/>
}, {
name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk',
element: <DiskGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
element: <DiskGraphTab data={data} dataSeries={parsedData} pluginHistorySeries={pluginHistorySeries}
loadingError={loadingError || pluginHistoryLoadingError}/>
},
]}/>
</Card>

View File

@ -1,9 +1,10 @@
import React from 'react';
import PerformanceAsNumbersTable from "../../../table/PerformanceAsNumbersTable";
import CardHeader from "../../CardHeader";
import {faBookOpen} from "@fortawesome/free-solid-svg-icons";
import {Card} from "react-bootstrap";
import {faBookOpen, faInfoCircle} from "@fortawesome/free-solid-svg-icons";
import {Alert, Card} from "react-bootstrap";
import {useTranslation} from "react-i18next";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
const PerformanceAsNumbersCard = ({data, servers}) => {
const {t} = useTranslation();
@ -15,11 +16,15 @@ const PerformanceAsNumbersCard = ({data, servers}) => {
: (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 dataIncludesGameServers = !servers || Boolean(servers.filter(server => !server.proxy).length);
return (
<Card id={"performance-as-numbers"}>
<CardHeader icon={faBookOpen} color="blue-grey" label={"html.label.performanceAsNumbers"}/>
{noDataAlert}
{!dataIncludesGameServers && <Alert className='alert-warning mb-0'>
<FontAwesomeIcon icon={faInfoCircle}/> {t('html.description.performanceNoGameServers')}
</Alert>}
<PerformanceAsNumbersTable data={data} servers={servers}/>
</Card>
)

View File

@ -62,7 +62,7 @@ const yAxis = [
}
]
const AllPerformanceGraph = ({id, data, dataSeries}) => {
const AllPerformanceGraph = ({id, data, dataSeries, pluginHistorySeries}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
const {timeZoneOffsetMinutes} = useMetadata();
@ -178,9 +178,9 @@ const AllPerformanceGraph = ({id, data, dataSeries}) => {
time: {
timezoneOffset: timeZoneOffsetMinutes
},
series: [series.playersOnline, series.tps, series.cpu, series.ram, series.entities, series.chunks]
series: [series.playersOnline, series.tps, series.cpu, series.ram, series.entities, series.chunks, pluginHistorySeries]
});
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t, timeZoneOffsetMinutes])
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t, timeZoneOffsetMinutes, pluginHistorySeries])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>

View File

@ -9,7 +9,7 @@ import {withReducedSaturation} from "../../../util/colors";
import Accessibility from "highcharts/modules/accessibility";
import {useMetadata} from "../../../hooks/metadataHook";
const CpuRamPerformanceGraph = ({id, data, dataSeries}) => {
const CpuRamPerformanceGraph = ({id, data, dataSeries, pluginHistorySeries}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
const {timeZoneOffsetMinutes} = useMetadata();
@ -90,9 +90,9 @@ const CpuRamPerformanceGraph = ({id, data, dataSeries}) => {
time: {
timezoneOffset: timeZoneOffsetMinutes
},
series: [series.playersOnline, series.cpu, series.ram]
series: [series.playersOnline, series.cpu, series.ram, pluginHistorySeries]
});
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t, timeZoneOffsetMinutes])
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t, timeZoneOffsetMinutes, pluginHistorySeries])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>

View File

@ -9,7 +9,7 @@ import {withReducedSaturation} from "../../../util/colors";
import Accessibility from "highcharts/modules/accessibility";
import {useMetadata} from "../../../hooks/metadataHook";
const DiskPerformanceGraph = ({id, data, dataSeries}) => {
const DiskPerformanceGraph = ({id, data, dataSeries, pluginHistorySeries}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
const {timeZoneOffsetMinutes} = useMetadata();
@ -71,9 +71,9 @@ const DiskPerformanceGraph = ({id, data, dataSeries}) => {
time: {
timezoneOffset: timeZoneOffsetMinutes
},
series: [series.disk]
series: [series.disk, pluginHistorySeries]
});
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t, timeZoneOffsetMinutes])
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t, timeZoneOffsetMinutes, pluginHistorySeries])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>

View File

@ -9,7 +9,7 @@ import {withReducedSaturation} from "../../../util/colors";
import Accessibility from "highcharts/modules/accessibility";
import {useMetadata} from "../../../hooks/metadataHook";
const TpsPerformanceGraph = ({id, data, dataSeries}) => {
const TpsPerformanceGraph = ({id, data, dataSeries, pluginHistorySeries}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
const {timeZoneOffsetMinutes} = useMetadata();
@ -87,9 +87,9 @@ const TpsPerformanceGraph = ({id, data, dataSeries}) => {
time: {
timezoneOffset: timeZoneOffsetMinutes
},
series: [series.playersOnline, series.tps]
series: [series.playersOnline, series.tps, pluginHistorySeries]
});
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t, timeZoneOffsetMinutes])
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t, timeZoneOffsetMinutes, pluginHistorySeries])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>

View File

@ -9,7 +9,7 @@ import {withReducedSaturation} from "../../../util/colors";
import Accessibility from "highcharts/modules/accessibility";
import {useMetadata} from "../../../hooks/metadataHook";
const WorldPerformanceGraph = ({id, data, dataSeries}) => {
const WorldPerformanceGraph = ({id, data, dataSeries, pluginHistorySeries}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
const {timeZoneOffsetMinutes} = useMetadata();
@ -89,9 +89,9 @@ const WorldPerformanceGraph = ({id, data, dataSeries}) => {
time: {
timezoneOffset: timeZoneOffsetMinutes
},
series: [series.playersOnline, series.entities, series.chunks]
series: [series.playersOnline, series.entities, series.chunks, pluginHistorySeries]
});
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t, timeZoneOffsetMinutes])
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t, timeZoneOffsetMinutes, pluginHistorySeries])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>

View File

@ -332,4 +332,10 @@ const fetchNetworkPlayerJoinAddresses = async (timestamp) => {
let url = `/v1/joinAddresses`;
if (staticSite) url = `/data/joinAddresses.json`;
return doGetRequest(url, timestamp);
}
export const fetchPluginHistory = async (timestamp, identifier) => {
let url = `/v1/pluginHistory?server=${identifier}`;
if (staticSite) url = `/data/pluginHistory-${identifier}.json`;
return doGetRequest(url, timestamp);
}

View File

@ -1495,4 +1495,8 @@ ul.filters {
.group-help i {
color: var(--color-plan)
}
.nav-item.disabled {
opacity: 30%;
}

View File

@ -4,6 +4,7 @@ import {Outlet} from "react-router-dom";
import {useNavigation} from "../../hooks/navigationHook";
import {
faChartLine,
faCodeCompare,
faCogs,
faCubes,
faGlobe,
@ -32,13 +33,14 @@ const HelpModal = React.lazy(() => import("../../components/modal/HelpModal"));
const NetworkSidebar = () => {
const {t, i18n} = useTranslation();
const {authRequired} = useAuth();
const {sidebarItems, setSidebarItems} = useNavigation();
const {networkMetadata} = useMetadata();
const {extensionData} = useServerExtensionContext();
useEffect(() => {
const servers = networkMetadata?.servers || [];
const items = [
let items = [
{
name: 'html.label.networkOverview',
icon: faInfoCircle,
@ -119,6 +121,13 @@ const NetworkSidebar = () => {
},
{},
{name: 'html.label.plugins', permission: 'page.network.plugins'},
{
name: 'html.label.pluginHistory',
icon: faCodeCompare,
href: "plugin-history",
permission: 'page.network.plugin.history',
authRequired: true
},
{
name: 'html.label.pluginsOverview',
icon: faCubes,
@ -147,10 +156,13 @@ const NetworkSidebar = () => {
{name: 'html.label.query', icon: faSearch, href: "/query", permission: 'access.query'}
);
}
// Filter out items that need authentication
items = items
.filter(item => !item.authRequired || (authRequired && item.authRequired))
setSidebarItems(items);
window.document.title = `Plan | Network`;
}, [t, i18n, extensionData, setSidebarItems, networkMetadata])
}, [t, i18n, extensionData, setSidebarItems, networkMetadata, authRequired])
return (
<Sidebar items={sidebarItems}/>

View File

@ -6,6 +6,7 @@ import {
faCampground,
faChartArea,
faChartLine,
faCodeCompare,
faCogs,
faCubes,
faGlobe,
@ -35,11 +36,12 @@ const HelpModal = React.lazy(() => import("../../components/modal/HelpModal"));
const ServerSidebar = () => {
const {t, i18n} = useTranslation();
const {authRequired} = useAuth();
const {sidebarItems, setSidebarItems} = useNavigation();
const {extensionData} = useServerExtensionContext();
useEffect(() => {
const items = [
let items = [
{
name: 'html.label.serverOverview',
icon: faInfoCircle,
@ -113,6 +115,13 @@ const ServerSidebar = () => {
{name: 'html.label.performance', icon: faCogs, href: "performance", permission: 'page.server.performance'},
{},
{name: 'html.label.plugins', permission: 'page.server.plugins'},
{
name: 'html.label.pluginHistory',
icon: faCodeCompare,
href: "plugin-history",
permission: 'page.server.plugin.history',
authRequired: true
},
{
name: 'html.label.pluginsOverview',
icon: faCubes,
@ -142,9 +151,12 @@ const ServerSidebar = () => {
);
}
// Filter out items that need authentication
items = items
.filter(item => !item.authRequired || (authRequired && item.authRequired))
setSidebarItems(items);
window.document.title = `Plan | Server Analysis`;
}, [t, i18n, extensionData, setSidebarItems])
}, [t, i18n, extensionData, setSidebarItems, authRequired])
return (
<Sidebar items={sidebarItems}/>

View File

@ -0,0 +1,72 @@
import {Col, InputGroup} from "react-bootstrap";
import React, {useEffect, useState} from "react";
import LoadIn from "../../components/animation/LoadIn";
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
import {useAuth} from "../../hooks/authenticationHook";
import PluginHistoryCard from "../../components/cards/common/PluginHistoryCard";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchPluginHistory} from "../../service/serverService";
import PluginCurrentCard from "../../components/cards/common/PluginCurrentCard";
import {useMetadata} from "../../hooks/metadataHook";
import Select from "../../components/input/Select";
import {useTranslation} from "react-i18next";
import InputGroupText from "react-bootstrap/InputGroupText";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faServer} from "@fortawesome/free-solid-svg-icons";
const NetworkPluginHistory = () => {
const {t} = useTranslation();
const {authRequired, hasPermission} = useAuth();
const seeHistory = authRequired && hasPermission('page.network.plugin.history');
const {networkMetadata} = useMetadata();
const [serverOptions, setServerOptions] = useState([]);
const [selectedOption, setSelectedOption] = useState(0);
const [identifier, setIdentifier] = useState(undefined);
useEffect(() => {
if (networkMetadata) {
const options = networkMetadata.servers;
setServerOptions(options);
const indexOfProxy = options
.findIndex(option => option.serverName === networkMetadata.currentServer.serverName);
setSelectedOption(indexOfProxy);
}
}, [networkMetadata, setSelectedOption, setServerOptions]);
useEffect(() => {
if (serverOptions.length) {
setIdentifier(serverOptions[selectedOption].serverUUID);
}
}, [selectedOption, serverOptions])
let {data, loadingError} = useDataRequest(fetchPluginHistory, [identifier], Boolean(identifier) && seeHistory);
if (!identifier) data = {history: []};
return (
<LoadIn>
{seeHistory && <section id="network-plugin-history">
<ExtendableRow id={'row-network-plugin-history-0'}>
<Col md={4} className={"mb-4"}>
<InputGroup>
<InputGroupText><FontAwesomeIcon icon={faServer}/> {t('html.label.serverSelector')}
</InputGroupText>
<Select options={serverOptions.map(server => server.serverName)}
selectedIndex={selectedOption} setSelectedIndex={setSelectedOption}/>
</InputGroup>
</Col>
</ExtendableRow>
<ExtendableRow id={'row-network-plugin-history-1'}>
<Col md={6}>
<PluginCurrentCard data={data} loadingError={loadingError}/>
</Col>
<Col md={6}>
<PluginHistoryCard data={data} loadingError={loadingError}/>
</Col>
</ExtendableRow>
</section>}
</LoadIn>
)
}
export default NetworkPluginHistory;

View File

@ -0,0 +1,34 @@
import {Col} from "react-bootstrap";
import React from "react";
import LoadIn from "../../components/animation/LoadIn";
import {useParams} from "react-router-dom";
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
import {useAuth} from "../../hooks/authenticationHook";
import PluginHistoryCard from "../../components/cards/common/PluginHistoryCard";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchPluginHistory} from "../../service/serverService";
import PluginCurrentCard from "../../components/cards/common/PluginCurrentCard";
const ServerPluginHistory = () => {
const {authRequired, hasPermission} = useAuth();
const {identifier} = useParams();
const seeHistory = authRequired && hasPermission('page.server.plugin.history');
const {data, loadingError} = useDataRequest(fetchPluginHistory, [identifier], seeHistory);
return (
<LoadIn>
{seeHistory && <section id="server-plugin-history">
<ExtendableRow id={'row-server-plugin-history-0'}>
<Col md={6}>
<PluginCurrentCard data={data} loadingError={loadingError}/>
</Col>
<Col md={6}>
<PluginHistoryCard data={data} loadingError={loadingError}/>
</Col>
</ExtendableRow>
</section>}
</LoadIn>
)
}
export default ServerPluginHistory;

View File

@ -16,10 +16,12 @@
*/
package com.djrapitops.plan.gathering;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import org.spongepowered.api.Game;
import org.spongepowered.api.entity.living.player.server.ServerPlayer;
import org.spongepowered.api.world.chunk.WorldChunk;
import org.spongepowered.api.world.server.ServerWorld;
import org.spongepowered.plugin.PluginContainer;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -82,4 +84,14 @@ public class SpongeSensor implements ServerSensor<ServerWorld> {
public List<String> getOnlinePlayerNames() {
return game.server().onlinePlayers().stream().map(ServerPlayer::name).collect(Collectors.toList());
}
@Override
public List<PluginMetadata> getInstalledPlugins() {
return game.pluginManager().plugins().stream()
.map(PluginContainer::metadata)
.map(metadata -> new PluginMetadata(
metadata.name().orElse(metadata.id()),
metadata.version().toString()))
.collect(Collectors.toList());
}
}

View File

@ -24,6 +24,7 @@ import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
import com.djrapitops.plan.gathering.ShutdownHook;
import com.djrapitops.plan.gathering.timed.InstalledPluginGatheringTask;
import com.djrapitops.plan.gathering.timed.ServerTPSCounter;
import com.djrapitops.plan.gathering.timed.SpongePingCounter;
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
@ -98,4 +99,8 @@ public interface SpongeTaskModule {
@Binds
@IntoSet
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
@Binds
@IntoSet
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
}

View File

@ -17,8 +17,10 @@
package com.djrapitops.plan.gathering;
import com.djrapitops.plan.PlanVelocity;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import com.djrapitops.plan.identification.properties.VelocityRedisCheck;
import com.djrapitops.plan.identification.properties.VelocityRedisPlayersOnlineSupplier;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.proxy.Player;
import javax.inject.Inject;
@ -34,6 +36,7 @@ public class VelocitySensor implements ServerSensor<Object> {
private final IntSupplier onlinePlayerCountSupplier;
private final Supplier<Collection<Player>> getPlayers;
private final Supplier<Collection<PluginContainer>> getPlugins;
@Inject
public VelocitySensor(PlanVelocity plugin) {
@ -41,6 +44,7 @@ public class VelocitySensor implements ServerSensor<Object> {
onlinePlayerCountSupplier = VelocityRedisCheck.isClassAvailable()
? new VelocityRedisPlayersOnlineSupplier()
: plugin.getProxy()::getPlayerCount;
getPlugins = () -> plugin.getProxy().getPluginManager().getPlugins();
}
@Override
@ -62,4 +66,14 @@ public class VelocitySensor implements ServerSensor<Object> {
public boolean usingRedisBungee() {
return VelocityRedisCheck.isClassAvailable();
}
@Override
public List<PluginMetadata> getInstalledPlugins() {
return getPlugins.get().stream()
.map(PluginContainer::getDescription)
.map(description -> new PluginMetadata(
description.getName().orElse(description.getId()),
description.getVersion().orElse("html.label.installed")))
.collect(Collectors.toList());
}
}

View File

@ -22,6 +22,7 @@ import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieExpiryCleanupTask
import com.djrapitops.plan.delivery.webserver.cache.JSONFileStorage;
import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
import com.djrapitops.plan.gathering.timed.InstalledPluginGatheringTask;
import com.djrapitops.plan.gathering.timed.ProxyTPSCounter;
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
import com.djrapitops.plan.gathering.timed.VelocityPingCounter;
@ -87,4 +88,8 @@ public interface VelocityTaskModule {
@Binds
@IntoSet
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
@Binds
@IntoSet
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
}