diff --git a/Plan/bukkit/src/main/java/com/djrapitops/plan/gathering/BukkitSensor.java b/Plan/bukkit/src/main/java/com/djrapitops/plan/gathering/BukkitSensor.java index 95a5c5e12..d47de9e8c 100644 --- a/Plan/bukkit/src/main/java/com/djrapitops/plan/gathering/BukkitSensor.java +++ b/Plan/bukkit/src/main/java/com/djrapitops/plan/gathering/BukkitSensor.java @@ -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 { .map(Player::getName) .collect(Collectors.toList()); } + + @Override + public List getInstalledPlugins() { + return Arrays.stream(Bukkit.getPluginManager().getPlugins()) + .map(Plugin::getDescription) + .map(description -> new PluginMetadata(description.getName(), description.getVersion())) + .collect(Collectors.toList()); + } } diff --git a/Plan/bukkit/src/main/java/com/djrapitops/plan/modules/bukkit/BukkitTaskModule.java b/Plan/bukkit/src/main/java/com/djrapitops/plan/modules/bukkit/BukkitTaskModule.java index b5292682e..e36510bbd 100644 --- a/Plan/bukkit/src/main/java/com/djrapitops/plan/modules/bukkit/BukkitTaskModule.java +++ b/Plan/bukkit/src/main/java/com/djrapitops/plan/modules/bukkit/BukkitTaskModule.java @@ -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); } diff --git a/Plan/bungeecord/src/main/java/com/djrapitops/plan/gathering/BungeeSensor.java b/Plan/bungeecord/src/main/java/com/djrapitops/plan/gathering/BungeeSensor.java index f66f3f8c5..b177c0688 100644 --- a/Plan/bungeecord/src/main/java/com/djrapitops/plan/gathering/BungeeSensor.java +++ b/Plan/bungeecord/src/main/java/com/djrapitops/plan/gathering/BungeeSensor.java @@ -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 { private final IntSupplier onlinePlayerCountSupplier; private final IntSupplier onlinePlayerCountBungee; private final Supplier> getPlayers; + private final Supplier> 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 { public boolean usingRedisBungee() { return RedisCheck.isClassAvailable(); } + + @Override + public List getInstalledPlugins() { + return getPlugins.get().stream() + .map(Plugin::getDescription) + .map(description -> new PluginMetadata(description.getName(), description.getVersion())) + .collect(Collectors.toList()); + } } diff --git a/Plan/bungeecord/src/main/java/com/djrapitops/plan/modules/bungee/BungeeTaskModule.java b/Plan/bungeecord/src/main/java/com/djrapitops/plan/modules/bungee/BungeeTaskModule.java index 73651757a..fb0d75f7f 100644 --- a/Plan/bungeecord/src/main/java/com/djrapitops/plan/modules/bungee/BungeeTaskModule.java +++ b/Plan/bungeecord/src/main/java/com/djrapitops/plan/modules/bungee/BungeeTaskModule.java @@ -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); } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/PluginHistoryMetadata.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/PluginHistoryMetadata.java new file mode 100644 index 000000000..7ce3dfb89 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/PluginHistoryMetadata.java @@ -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 . + */ +package com.djrapitops.plan.delivery.domain; + +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +/** + * Represents plugin version history. + *

+ * 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 + + '}'; + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java index a8aae82f8..175e578de 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java @@ -55,6 +55,7 @@ public enum WebPermission implements Supplier, 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, 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"), diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/PluginHistoryDto.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/PluginHistoryDto.java new file mode 100644 index 000000000..e9838b685 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/PluginHistoryDto.java @@ -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 . + */ +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 history; + + public PluginHistoryDto(List history) { + this.history = history; + } + + public List 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 + + '}'; + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java index 17d8b4385..5ff2c5fd7 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java @@ -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(); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/plugins/PluginHistoryJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/plugins/PluginHistoryJSONResolver.java new file mode 100644 index 000000000..b02255ebe --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/plugins/PluginHistoryJSONResolver.java @@ -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 . + */ +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 resolve(@Untrusted Request request) { + return Optional.of(getResponse(request)); + } + + private Response getResponse(@Untrusted Request request) { + ServerUUID serverUUID = identifiers.getServerUUID(request); // Can throw BadRequestException + List history = dbSystem.getDatabase().query(PluginMetadataQueries.getPluginHistory(serverUUID)); + return Response.builder() + .setMimeType(MimeType.JSON) + .setJSONContent(new PluginHistoryDto(history)) + .build(); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/gathering/ServerSensor.java b/Plan/common/src/main/java/com/djrapitops/plan/gathering/ServerSensor.java index 0cece79b6..d3f99756e 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/gathering/ServerSensor.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/gathering/ServerSensor.java @@ -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 { default boolean usingRedisBungee() { return false; } + + default List getInstalledPlugins() { + return List.of(); + } } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/gathering/domain/PluginMetadata.java b/Plan/common/src/main/java/com/djrapitops/plan/gathering/domain/PluginMetadata.java new file mode 100644 index 000000000..16c113346 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/gathering/domain/PluginMetadata.java @@ -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 . + */ +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 + '\'' + + '}'; + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/gathering/timed/InstalledPluginGatheringTask.java b/Plan/common/src/main/java/com/djrapitops/plan/gathering/timed/InstalledPluginGatheringTask.java new file mode 100644 index 000000000..444ad6a3b --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/gathering/timed/InstalledPluginGatheringTask.java @@ -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 . + */ +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 installedPlugins = serverSensor.getInstalledPlugins(); + + ServerUUID serverUUID = serverInfo.getServerUUID(); + List previouslyInstalledPlugins = dbSystem.getDatabase() + .query(PluginMetadataQueries.getInstalledPlugins(serverUUID)); + + List newPlugins = new ArrayList<>(); + List updatedPlugins = new ArrayList<>(); + + Set installedPluginNames = new HashSet<>(); + for (PluginMetadata installedPlugin : installedPlugins) { + installedPluginNames.add(installedPlugin.getName()); + + Optional 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 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 pluginChangeList = new ArrayList<>(); + pluginChangeList.addAll(newPlugins); + pluginChangeList.addAll(updatedPlugins); + pluginChangeList.addAll(removedPlugins); + + dbSystem.getDatabase().executeTransaction(new StorePluginVersionsTransaction(enableTime, serverUUID, pluginChangeList)); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/config/changes/ConfigChange.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/config/changes/ConfigChange.java index d474e29e7..5dd3a79e9 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/config/changes/ConfigChange.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/config/changes/ConfigChange.java @@ -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(); } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java index c34953bbf..c6b728d38 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java @@ -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"), diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/PluginMetadataQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/PluginMetadataQueries.java new file mode 100644 index 000000000..602bc8fd0 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/PluginMetadataQueries.java @@ -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 . + */ +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> 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 processResults(ResultSet set) throws SQLException { + Set foundPlugins = new HashSet<>(); + List 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> 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); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/PluginVersionTable.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/PluginVersionTable.java new file mode 100644 index 000000000..b94a4100e --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/PluginVersionTable.java @@ -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 . + */ +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. + *

+ * 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(); + } + +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/StorePluginVersionsTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/StorePluginVersionsTransaction.java new file mode 100644 index 000000000..e248580a7 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/StorePluginVersionsTransaction.java @@ -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 . + */ +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 changeList; + + public StorePluginVersionsTransaction(long time, ServerUUID serverUUID, List 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 getChangeList() { + return changeList; + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/init/CreateTablesTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/init/CreateTablesTransaction.java index 1b84c9b7a..5370436ac 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/init/CreateTablesTransaction.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/init/CreateTablesTransaction.java @@ -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)); diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml index 689cdbbcf..95f2fbfad 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml @@ -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: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml index 3bfd2a640..9e54d7979 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml @@ -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'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml index fe97e5fed..59c41edf9 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml @@ -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'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml index 701232376..ce7df2bad 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml @@ -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'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml index d39d66b23..2ae84fa81 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml @@ -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'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml index 6bd38e317..e4094ec2e 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml @@ -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'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml index dc17b54d4..219689e7f 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml @@ -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'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml index 1f7a7ee40..0e6ff7d13 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml @@ -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'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml index 8cae901ce..bc573a28c 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml @@ -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: "'月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml index df4961c4c..72086c635 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml @@ -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: "'월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml index 95096eb4d..b1b0ee703 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml @@ -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'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml index 27dc9adad..84e3fcb00 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml @@ -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'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml index 24bac2253..6c0f23a9e 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml @@ -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: "'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml index 1d7e33793..5417751b0 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml @@ -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'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml index 18bd8bae0..6e807e06d 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml @@ -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: "'Понеділок', 'Вівторок', 'Середа', 'Четвер', 'П`ятниця', 'Субота', 'Неділя'" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml index 6e06bf77b..12f8f2128 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml @@ -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: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'" diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java index d83a4cad7..df9ae6071 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java @@ -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) ); } diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java index 0d9033ace..994d4891f 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java @@ -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); diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java index 60c7fb894..48404f9e2 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java @@ -91,6 +91,7 @@ class HttpAccessControlTest { "/v1/saveGroupPermissions", "/v1/deleteGroup", "/v1/storePreferences", + "/v1/pluginHistory?server=" + TestConstants.SERVER_UUID_STRING, "/manage", "/auth/register", "/auth/login", diff --git a/Plan/common/src/test/java/com/djrapitops/plan/gathering/timed/InstalledPluginGatheringTaskTest.java b/Plan/common/src/test/java/com/djrapitops/plan/gathering/timed/InstalledPluginGatheringTaskTest.java new file mode 100644 index 000000000..1d2b3ce5b --- /dev/null +++ b/Plan/common/src/test/java/com/djrapitops/plan/gathering/timed/InstalledPluginGatheringTaskTest.java @@ -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 . + */ +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 previouslyInstalledPlugins = List.of(); + List 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 changeList = ((StorePluginVersionsTransaction) capturedTransaction).getChangeList(); + assertEquals(installedPlugins, changeList); + } + + + @Test + void onlyUpdatedPluginsAreIncluded() { + List previouslyInstalledPlugins = List.of( + new PluginMetadata("Plan", "5.6 build 2121"), + new PluginMetadata("LittleChef", "1.0.2") + ); + List 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 expected = List.of( + new PluginMetadata("LittleChef", "1.0.3") + ); + List changeList = ((StorePluginVersionsTransaction) capturedTransaction).getChangeList(); + assertEquals(expected, changeList); + } + + + @Test + void removedPluginsAreIncluded() { + List previouslyInstalledPlugins = List.of( + new PluginMetadata("Plan", "5.6 build 2121"), + new PluginMetadata("LittleChef", "1.0.2") + ); + List installedPlugins = List.of( + new PluginMetadata("Plan", "5.6 build 2121") + ); + when(database.query(any())).thenReturn(previouslyInstalledPlugins); + when(serverSensor.getInstalledPlugins()).thenReturn(installedPlugins); + + underTest.run(); + + List expected = List.of( + new PluginMetadata("LittleChef", null) + ); + List changeList = ((StorePluginVersionsTransaction) capturedTransaction).getChangeList(); + assertEquals(expected, changeList); + } +} \ No newline at end of file diff --git a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/DatabaseTestAggregate.java b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/DatabaseTestAggregate.java index 2f8de4fb3..33458fdda 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/DatabaseTestAggregate.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/DatabaseTestAggregate.java @@ -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 */ } diff --git a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/objects/PluginMetadataQueriesTest.java b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/objects/PluginMetadataQueriesTest.java new file mode 100644 index 000000000..0f91b1110 --- /dev/null +++ b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/objects/PluginMetadataQueriesTest.java @@ -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 . + */ +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 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 expected = List.of( + new PluginMetadata("Plan", "5.6 build 2121"), + new PluginMetadata("LittleChef", "1.0.2") + ); + List result = db().query(PluginMetadataQueries.getInstalledPlugins(serverUUID())); + assertEquals(expected, result); + } + +} \ No newline at end of file diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/FabricSensor.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/FabricSensor.java index 6e15bc586..0db763447 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/FabricSensor.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/gathering/FabricSensor.java @@ -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 { public List getOnlinePlayerNames() { return Arrays.asList(server.getPlayerNames()); } + + @Override + public List getInstalledPlugins() { + return FabricLoaderImpl.INSTANCE.getMods().stream() + .map(ModContainer::getMetadata) + .map(metadata -> new PluginMetadata( + Optional.ofNullable(metadata.getName()).orElse(metadata.getId()), + metadata.getVersion().getFriendlyString())) + .toList(); + } } diff --git a/Plan/fabric/src/main/java/net/playeranalytics/plan/modules/fabric/FabricTaskModule.java b/Plan/fabric/src/main/java/net/playeranalytics/plan/modules/fabric/FabricTaskModule.java index d87b6fad3..b9163bbb6 100644 --- a/Plan/fabric/src/main/java/net/playeranalytics/plan/modules/fabric/FabricTaskModule.java +++ b/Plan/fabric/src/main/java/net/playeranalytics/plan/modules/fabric/FabricTaskModule.java @@ -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); } diff --git a/Plan/nukkit/src/main/java/com/djrapitops/plan/gathering/NukkitSensor.java b/Plan/nukkit/src/main/java/com/djrapitops/plan/gathering/NukkitSensor.java index e395e3956..45c2cae7c 100644 --- a/Plan/nukkit/src/main/java/com/djrapitops/plan/gathering/NukkitSensor.java +++ b/Plan/nukkit/src/main/java/com/djrapitops/plan/gathering/NukkitSensor.java @@ -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 { .map(Player::getName) .collect(Collectors.toList()); } + + @Override + public List getInstalledPlugins() { + return plugin.getServer().getPluginManager() + .getPlugins().values().stream() + .map(Plugin::getDescription) + .map(description -> new PluginMetadata(description.getName(), description.getVersion())) + .collect(Collectors.toList()); + } } diff --git a/Plan/nukkit/src/main/java/com/djrapitops/plan/modules/nukkit/NukkitTaskModule.java b/Plan/nukkit/src/main/java/com/djrapitops/plan/modules/nukkit/NukkitTaskModule.java index d3a3799be..2d9197606 100644 --- a/Plan/nukkit/src/main/java/com/djrapitops/plan/modules/nukkit/NukkitTaskModule.java +++ b/Plan/nukkit/src/main/java/com/djrapitops/plan/modules/nukkit/NukkitTaskModule.java @@ -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); } diff --git a/Plan/react/dashboard/src/App.js b/Plan/react/dashboard/src/App.js index 75694b78c..c90e8b574 100644 --- a/Plan/react/dashboard/src/App.js +++ b/Plan/react/dashboard/src/App.js @@ -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() { }/> }/> }/> + }/> }/> }/> }/> }/> }/> + }/> }/> }/> { +const TabButton = ({id, name, href, icon, color, active, disabled}) => { const navigate = useNavigate(); return ( -

  • - @@ -26,6 +27,7 @@ const TabButtons = ({tabs, selectedTab}) => { icon={tab.icon} color={tab.color} active={tab.href === selectedTab} + disabled={tab.disabled} /> ))} diff --git a/Plan/react/dashboard/src/components/cards/common/PluginCurrentCard.js b/Plan/react/dashboard/src/components/cards/common/PluginCurrentCard.js new file mode 100644 index 000000000..b6dbb856e --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/common/PluginCurrentCard.js @@ -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 ; + if (!data) return ; + + const history = []; + + for (const entry of data.history) { + if (history.find(e => e.name === entry.name)) continue; + history.push(entry); + } + + const table = { + columns: [{ + title: <> {t('html.label.name')}, + data: "name" + }, { + title: <> {t('html.label.version')}, + data: "version" + }, { + title: <> {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: + } + }) : [{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 ( + + + + + ) +}; + +export default PluginCurrentCard \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/common/PluginHistoryCard.js b/Plan/react/dashboard/src/components/cards/common/PluginHistoryCard.js new file mode 100644 index 000000000..a8861e022 --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/common/PluginHistoryCard.js @@ -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 ; + if (!data) return ; + + const history = data.history; + + const table = { + columns: [{ + title: <> {t('html.label.name')}, + data: "name" + }, { + title: <> {t('html.label.version')}, + data: "version" + }, { + title: <> {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: + } + }) : [{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 ( + + + + + ) +}; + +export default PluginHistoryCard \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/network/PerformanceGraphsCard.js b/Plan/react/dashboard/src/components/cards/network/PerformanceGraphsCard.js index 783bccdb3..6fd6f12e0 100644 --- a/Plan/react/dashboard/src/components/cards/network/PerformanceGraphsCard.js +++ b/Plan/react/dashboard/src/components/cards/network/PerformanceGraphsCard.js @@ -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: }, { name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps', - element: + element: , + disabled: !dataIncludesGameServers }, { name: t('html.label.cpu'), icon: faTachometerAlt, color: 'amber', href: 'cpu', element: @@ -168,10 +171,12 @@ const PerformanceGraphsCard = ({data}) => { element: }, { name: t('html.label.entities'), icon: faDragon, color: 'purple', href: 'entities', - element: + element: , + disabled: !dataIncludesGameServers }, { name: t('html.label.loadedChunks'), icon: faMap, color: 'blue-grey', href: 'chunks', - element: + element: , + disabled: !dataIncludesGameServers }, { name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk', element: @@ -180,7 +185,7 @@ const PerformanceGraphsCard = ({data}) => { element: networkMetadata ? : }, - ], [performanceSeries, networkMetadata, t]); + ], [performanceSeries, networkMetadata, t, dataIncludesGameServers]); if (!data || !Object.values(data).length) return if (data.errors.length) { diff --git a/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js b/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js index 0a75a0d31..75ef173d8 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js +++ b/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js @@ -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 if (!dataSeries) return ; - return + return } -const TpsGraphTab = ({data, dataSeries, loadingError}) => { +const TpsGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => { if (loadingError) return if (!dataSeries) return ; - return + return } -const CpuRamGraphTab = ({data, dataSeries, loadingError}) => { +const CpuRamGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => { if (loadingError) return if (!dataSeries) return ; - return + return } -const WorldGraphTab = ({data, dataSeries, loadingError}) => { +const WorldGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => { if (loadingError) return if (!dataSeries) return ; - return + return } -const DiskGraphTab = ({data, dataSeries, loadingError}) => { +const DiskGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => { if (loadingError) return if (!dataSeries) return ; - return + return } 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(',
    '); + const uninstalledLines = entry[1].filter(p => !p.version).map(plugin => plugin.name).join(',
    '); + return { + x: entry[0], + title: entry[1].length, + text: (installedLines.length ? '' + t('html.label.installed') + '
    ' + installedLines : '') + + (uninstalledLines.length ? '' + t('html.label.uninstalled') + '
    ' + uninstalledLines : '') + } + }) + }) + } + }, [pluginHistory, setPluginHistorySeries, t]); return + element: }, { name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps', - element: + element: }, { name: t('html.label.cpuRam'), icon: faMicrochip, color: 'light-green', href: 'cpu-ram', - element: + element: }, { name: t('html.label.world'), icon: faMap, color: 'purple', href: 'world-load', - element: + element: }, { name: t('html.label.ping'), icon: faSignal, color: 'amber', href: 'ping', element: }, { name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk', - element: + element: }, ]}/> diff --git a/Plan/react/dashboard/src/components/cards/server/tables/PerformanceAsNumbersCard.js b/Plan/react/dashboard/src/components/cards/server/tables/PerformanceAsNumbersCard.js index 2b3305144..7c08a70b0 100644 --- a/Plan/react/dashboard/src/components/cards/server/tables/PerformanceAsNumbersCard.js +++ b/Plan/react/dashboard/src/components/cards/server/tables/PerformanceAsNumbersCard.js @@ -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 ?

    {t('html.description.noData7d')}

    : (noData24h ?

    {t('html.description.noData24h')}

    : '')); + const dataIncludesGameServers = !servers || Boolean(servers.filter(server => !server.proxy).length); return ( {noDataAlert} + {!dataIncludesGameServers && + {t('html.description.performanceNoGameServers')} + } ) diff --git a/Plan/react/dashboard/src/components/graphs/performance/AllPerformanceGraph.js b/Plan/react/dashboard/src/components/graphs/performance/AllPerformanceGraph.js index 7e8130d1a..5fe38ff20 100644 --- a/Plan/react/dashboard/src/components/graphs/performance/AllPerformanceGraph.js +++ b/Plan/react/dashboard/src/components/graphs/performance/AllPerformanceGraph.js @@ -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 (
    diff --git a/Plan/react/dashboard/src/components/graphs/performance/CpuRamPerformanceGraph.js b/Plan/react/dashboard/src/components/graphs/performance/CpuRamPerformanceGraph.js index c23a9609f..ed491b544 100644 --- a/Plan/react/dashboard/src/components/graphs/performance/CpuRamPerformanceGraph.js +++ b/Plan/react/dashboard/src/components/graphs/performance/CpuRamPerformanceGraph.js @@ -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 (
    diff --git a/Plan/react/dashboard/src/components/graphs/performance/DiskPerformanceGraph.js b/Plan/react/dashboard/src/components/graphs/performance/DiskPerformanceGraph.js index 27e012c2c..e0a532328 100644 --- a/Plan/react/dashboard/src/components/graphs/performance/DiskPerformanceGraph.js +++ b/Plan/react/dashboard/src/components/graphs/performance/DiskPerformanceGraph.js @@ -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 (
    diff --git a/Plan/react/dashboard/src/components/graphs/performance/TpsPerformanceGraph.js b/Plan/react/dashboard/src/components/graphs/performance/TpsPerformanceGraph.js index fc0a2f74a..a34d574a0 100644 --- a/Plan/react/dashboard/src/components/graphs/performance/TpsPerformanceGraph.js +++ b/Plan/react/dashboard/src/components/graphs/performance/TpsPerformanceGraph.js @@ -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 (
    diff --git a/Plan/react/dashboard/src/components/graphs/performance/WorldPerformanceGraph.js b/Plan/react/dashboard/src/components/graphs/performance/WorldPerformanceGraph.js index 05681a963..75ff7640a 100644 --- a/Plan/react/dashboard/src/components/graphs/performance/WorldPerformanceGraph.js +++ b/Plan/react/dashboard/src/components/graphs/performance/WorldPerformanceGraph.js @@ -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 (
    diff --git a/Plan/react/dashboard/src/service/serverService.js b/Plan/react/dashboard/src/service/serverService.js index 43ef0731b..f60fd9650 100644 --- a/Plan/react/dashboard/src/service/serverService.js +++ b/Plan/react/dashboard/src/service/serverService.js @@ -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); } \ No newline at end of file diff --git a/Plan/react/dashboard/src/style/style.css b/Plan/react/dashboard/src/style/style.css index ca4bf850c..aed19cd60 100644 --- a/Plan/react/dashboard/src/style/style.css +++ b/Plan/react/dashboard/src/style/style.css @@ -1495,4 +1495,8 @@ ul.filters { .group-help i { color: var(--color-plan) +} + +.nav-item.disabled { + opacity: 30%; } \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/layout/NetworkPage.js b/Plan/react/dashboard/src/views/layout/NetworkPage.js index 795df9486..693608c52 100644 --- a/Plan/react/dashboard/src/views/layout/NetworkPage.js +++ b/Plan/react/dashboard/src/views/layout/NetworkPage.js @@ -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 ( diff --git a/Plan/react/dashboard/src/views/layout/ServerPage.js b/Plan/react/dashboard/src/views/layout/ServerPage.js index 11413c2a1..fbb2f1c14 100644 --- a/Plan/react/dashboard/src/views/layout/ServerPage.js +++ b/Plan/react/dashboard/src/views/layout/ServerPage.js @@ -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 ( diff --git a/Plan/react/dashboard/src/views/network/NetworkPluginHistory.js b/Plan/react/dashboard/src/views/network/NetworkPluginHistory.js new file mode 100644 index 000000000..a2d50ef84 --- /dev/null +++ b/Plan/react/dashboard/src/views/network/NetworkPluginHistory.js @@ -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 ( + + {seeHistory &&
    + + + + {t('html.label.serverSelector')} + +