2360/plugin versions (#3249)
* Add methods to gather plugin versions from servers * Gathering and storage for plugin version history * Test plugin gathering * Test plugin metadata storage * /v1/pluginHistory endpoint * Plugin history tab * Plugin history to performance tab * Possibly fix ConfigChange.MovedValue being applied all the time * Updated locale files * Export pluginHistory for server page * Add plugin history to network page * Access control and improvements * Remove pluginHistory from export since it now requires auth * Fix access visibility tests * Fix VelocitySensor during test Affects issues: - Close #2360
This commit is contained in:
parent
61db13626b
commit
5a2bdaf6ba
|
@ -16,13 +16,16 @@
|
||||||
*/
|
*/
|
||||||
package com.djrapitops.plan.gathering;
|
package com.djrapitops.plan.gathering;
|
||||||
|
|
||||||
|
import com.djrapitops.plan.gathering.domain.PluginMetadata;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -129,4 +132,12 @@ public class BukkitSensor implements ServerSensor<World> {
|
||||||
.map(Player::getName)
|
.map(Player::getName)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PluginMetadata> getInstalledPlugins() {
|
||||||
|
return Arrays.stream(Bukkit.getPluginManager().getPlugins())
|
||||||
|
.map(Plugin::getDescription)
|
||||||
|
.map(description -> new PluginMetadata(description.getName(), description.getVersion()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
|
||||||
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
|
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
|
||||||
import com.djrapitops.plan.gathering.ShutdownHook;
|
import com.djrapitops.plan.gathering.ShutdownHook;
|
||||||
import com.djrapitops.plan.gathering.timed.BukkitPingCounter;
|
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.ServerTPSCounter;
|
||||||
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
||||||
import com.djrapitops.plan.settings.upkeep.ConfigStoreTask;
|
import com.djrapitops.plan.settings.upkeep.ConfigStoreTask;
|
||||||
|
@ -108,4 +109,8 @@ public interface BukkitTaskModule {
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,11 @@
|
||||||
package com.djrapitops.plan.gathering;
|
package com.djrapitops.plan.gathering;
|
||||||
|
|
||||||
import com.djrapitops.plan.PlanBungee;
|
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.RedisCheck;
|
||||||
import com.djrapitops.plan.identification.properties.RedisPlayersOnlineSupplier;
|
import com.djrapitops.plan.identification.properties.RedisPlayersOnlineSupplier;
|
||||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
@ -35,11 +37,13 @@ public class BungeeSensor implements ServerSensor<Object> {
|
||||||
private final IntSupplier onlinePlayerCountSupplier;
|
private final IntSupplier onlinePlayerCountSupplier;
|
||||||
private final IntSupplier onlinePlayerCountBungee;
|
private final IntSupplier onlinePlayerCountBungee;
|
||||||
private final Supplier<Collection<ProxiedPlayer>> getPlayers;
|
private final Supplier<Collection<ProxiedPlayer>> getPlayers;
|
||||||
|
private final Supplier<Collection<Plugin>> getPlugins;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public BungeeSensor(PlanBungee plugin) {
|
public BungeeSensor(PlanBungee plugin) {
|
||||||
getPlayers = plugin.getProxy()::getPlayers;
|
getPlayers = plugin.getProxy()::getPlayers;
|
||||||
onlinePlayerCountBungee = plugin.getProxy()::getOnlineCount;
|
onlinePlayerCountBungee = plugin.getProxy()::getOnlineCount;
|
||||||
|
getPlugins = plugin.getProxy().getPluginManager()::getPlugins;
|
||||||
onlinePlayerCountSupplier = RedisCheck.isClassAvailable() ? new RedisPlayersOnlineSupplier() : onlinePlayerCountBungee;
|
onlinePlayerCountSupplier = RedisCheck.isClassAvailable() ? new RedisPlayersOnlineSupplier() : onlinePlayerCountBungee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,4 +67,12 @@ public class BungeeSensor implements ServerSensor<Object> {
|
||||||
public boolean usingRedisBungee() {
|
public boolean usingRedisBungee() {
|
||||||
return RedisCheck.isClassAvailable();
|
return RedisCheck.isClassAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PluginMetadata> getInstalledPlugins() {
|
||||||
|
return getPlugins.get().stream()
|
||||||
|
.map(Plugin::getDescription)
|
||||||
|
.map(description -> new PluginMetadata(description.getName(), description.getVersion()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import com.djrapitops.plan.delivery.webserver.cache.JSONFileStorage;
|
||||||
import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
|
import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
|
||||||
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
|
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
|
||||||
import com.djrapitops.plan.gathering.timed.BungeePingCounter;
|
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.ProxyTPSCounter;
|
||||||
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
||||||
import com.djrapitops.plan.settings.upkeep.NetworkConfigStoreTask;
|
import com.djrapitops.plan.settings.upkeep.NetworkConfigStoreTask;
|
||||||
|
@ -87,4 +88,8 @@ public interface BungeeTaskModule {
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Player Analytics (Plan).
|
||||||
|
*
|
||||||
|
* Plan is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Plan is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.djrapitops.plan.delivery.domain;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents plugin version history.
|
||||||
|
* <p>
|
||||||
|
* If version is null the plugin was uninstalled at that time.
|
||||||
|
*
|
||||||
|
* @author AuroraLS3
|
||||||
|
*/
|
||||||
|
public class PluginHistoryMetadata {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
@Nullable
|
||||||
|
private final String version;
|
||||||
|
private final long modified;
|
||||||
|
|
||||||
|
public PluginHistoryMetadata(String name, @Nullable String version, long modified) {
|
||||||
|
this.name = name;
|
||||||
|
this.version = version;
|
||||||
|
this.modified = modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getModified() {
|
||||||
|
return modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
PluginHistoryMetadata that = (PluginHistoryMetadata) o;
|
||||||
|
return getModified() == that.getModified() && Objects.equals(getName(), that.getName()) && Objects.equals(getVersion(), that.getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(getName(), getVersion(), getModified());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "PluginHistoryMetadata{" +
|
||||||
|
"name='" + name + '\'' +
|
||||||
|
", version='" + version + '\'' +
|
||||||
|
", modified=" + modified +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,6 +55,7 @@ public enum WebPermission implements Supplier<String>, Lang {
|
||||||
PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY("See Ping Per Country table"),
|
PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY("See Ping Per Country table"),
|
||||||
PAGE_NETWORK_PLAYERS("See Player list -tab"),
|
PAGE_NETWORK_PLAYERS("See Player list -tab"),
|
||||||
PAGE_NETWORK_PERFORMANCE("See network Performance 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_NETWORK_PLUGINS("See Plugins tab of Proxy"),
|
||||||
|
|
||||||
PAGE_SERVER("See all of server page"),
|
PAGE_SERVER("See all of server page"),
|
||||||
|
@ -90,6 +91,7 @@ public enum WebPermission implements Supplier<String>, Lang {
|
||||||
PAGE_SERVER_PERFORMANCE("See Performance tab"),
|
PAGE_SERVER_PERFORMANCE("See Performance tab"),
|
||||||
PAGE_SERVER_PERFORMANCE_GRAPHS("See Performance graphs"),
|
PAGE_SERVER_PERFORMANCE_GRAPHS("See Performance graphs"),
|
||||||
PAGE_SERVER_PERFORMANCE_OVERVIEW("See Performance numbers"),
|
PAGE_SERVER_PERFORMANCE_OVERVIEW("See Performance numbers"),
|
||||||
|
PAGE_SERVER_PLUGIN_HISTORY("See Plugin History"),
|
||||||
PAGE_SERVER_PLUGINS("See Plugins -tabs of servers"),
|
PAGE_SERVER_PLUGINS("See Plugins -tabs of servers"),
|
||||||
|
|
||||||
PAGE_PLAYER("See all of player page"),
|
PAGE_PLAYER("See all of player page"),
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Player Analytics (Plan).
|
||||||
|
*
|
||||||
|
* Plan is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Plan is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.djrapitops.plan.delivery.domain.datatransfer;
|
||||||
|
|
||||||
|
import com.djrapitops.plan.delivery.domain.PluginHistoryMetadata;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History of plugin versions, sorted most recent first.
|
||||||
|
*
|
||||||
|
* @author AuroraLS3
|
||||||
|
*/
|
||||||
|
public class PluginHistoryDto {
|
||||||
|
|
||||||
|
private final List<PluginHistoryMetadata> history;
|
||||||
|
|
||||||
|
public PluginHistoryDto(List<PluginHistoryMetadata> history) {
|
||||||
|
this.history = history;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PluginHistoryMetadata> getHistory() {
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
PluginHistoryDto that = (PluginHistoryDto) o;
|
||||||
|
return Objects.equals(getHistory(), that.getHistory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(getHistory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "PluginHistoryDto{" +
|
||||||
|
"history=" + history +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.http.WebServer;
|
||||||
import com.djrapitops.plan.delivery.webserver.resolver.json.metadata.PreferencesJSONResolver;
|
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.metadata.StorePreferencesJSONResolver;
|
||||||
|
import com.djrapitops.plan.delivery.webserver.resolver.json.plugins.PluginHistoryJSONResolver;
|
||||||
import com.djrapitops.plan.identification.Identifiers;
|
import com.djrapitops.plan.identification.Identifiers;
|
||||||
import dagger.Lazy;
|
import dagger.Lazy;
|
||||||
|
|
||||||
|
@ -49,6 +50,7 @@ public class RootJSONResolver {
|
||||||
|
|
||||||
private final CompositeResolver.Builder readOnlyResourcesBuilder;
|
private final CompositeResolver.Builder readOnlyResourcesBuilder;
|
||||||
private final StorePreferencesJSONResolver storePreferencesJSONResolver;
|
private final StorePreferencesJSONResolver storePreferencesJSONResolver;
|
||||||
|
private final PluginHistoryJSONResolver pluginHistoryJSONResolver;
|
||||||
private CompositeResolver resolver;
|
private CompositeResolver resolver;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -84,6 +86,7 @@ public class RootJSONResolver {
|
||||||
ExtensionJSONResolver extensionJSONResolver,
|
ExtensionJSONResolver extensionJSONResolver,
|
||||||
RetentionJSONResolver retentionJSONResolver,
|
RetentionJSONResolver retentionJSONResolver,
|
||||||
PlayerJoinAddressJSONResolver playerJoinAddressJSONResolver,
|
PlayerJoinAddressJSONResolver playerJoinAddressJSONResolver,
|
||||||
|
PluginHistoryJSONResolver pluginHistoryJSONResolver,
|
||||||
|
|
||||||
PreferencesJSONResolver preferencesJSONResolver,
|
PreferencesJSONResolver preferencesJSONResolver,
|
||||||
StorePreferencesJSONResolver storePreferencesJSONResolver,
|
StorePreferencesJSONResolver storePreferencesJSONResolver,
|
||||||
|
@ -127,6 +130,7 @@ public class RootJSONResolver {
|
||||||
|
|
||||||
this.webServer = webServer;
|
this.webServer = webServer;
|
||||||
// These endpoints require authentication to be enabled.
|
// These endpoints require authentication to be enabled.
|
||||||
|
this.pluginHistoryJSONResolver = pluginHistoryJSONResolver;
|
||||||
this.webGroupJSONResolver = webGroupJSONResolver;
|
this.webGroupJSONResolver = webGroupJSONResolver;
|
||||||
this.webGroupPermissionJSONResolver = webGroupPermissionJSONResolver;
|
this.webGroupPermissionJSONResolver = webGroupPermissionJSONResolver;
|
||||||
this.webPermissionJSONResolver = webPermissionJSONResolver;
|
this.webPermissionJSONResolver = webPermissionJSONResolver;
|
||||||
|
@ -149,6 +153,7 @@ public class RootJSONResolver {
|
||||||
.add("saveGroupPermissions", webGroupSaveJSONResolver)
|
.add("saveGroupPermissions", webGroupSaveJSONResolver)
|
||||||
.add("deleteGroup", webGroupDeleteJSONResolver)
|
.add("deleteGroup", webGroupDeleteJSONResolver)
|
||||||
.add("storePreferences", storePreferencesJSONResolver)
|
.add("storePreferences", storePreferencesJSONResolver)
|
||||||
|
.add("pluginHistory", pluginHistoryJSONResolver)
|
||||||
.build();
|
.build();
|
||||||
} else {
|
} else {
|
||||||
resolver = readOnlyResourcesBuilder.build();
|
resolver = readOnlyResourcesBuilder.build();
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Player Analytics (Plan).
|
||||||
|
*
|
||||||
|
* Plan is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Plan is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.djrapitops.plan.delivery.webserver.resolver.json.plugins;
|
||||||
|
|
||||||
|
import com.djrapitops.plan.delivery.domain.PluginHistoryMetadata;
|
||||||
|
import com.djrapitops.plan.delivery.domain.auth.WebPermission;
|
||||||
|
import com.djrapitops.plan.delivery.domain.datatransfer.PluginHistoryDto;
|
||||||
|
import com.djrapitops.plan.delivery.web.resolver.MimeType;
|
||||||
|
import com.djrapitops.plan.delivery.web.resolver.Resolver;
|
||||||
|
import com.djrapitops.plan.delivery.web.resolver.Response;
|
||||||
|
import com.djrapitops.plan.delivery.web.resolver.request.Request;
|
||||||
|
import com.djrapitops.plan.identification.Identifiers;
|
||||||
|
import com.djrapitops.plan.identification.ServerUUID;
|
||||||
|
import com.djrapitops.plan.storage.database.DBSystem;
|
||||||
|
import com.djrapitops.plan.storage.database.queries.objects.PluginMetadataQueries;
|
||||||
|
import com.djrapitops.plan.utilities.dev.Untrusted;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint for getting plugin version history.
|
||||||
|
*
|
||||||
|
* @author AuroraLS3
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
@Path("/v1/pluginHistory")
|
||||||
|
public class PluginHistoryJSONResolver implements Resolver {
|
||||||
|
|
||||||
|
private final DBSystem dbSystem;
|
||||||
|
private final Identifiers identifiers;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PluginHistoryJSONResolver(DBSystem dbSystem, Identifiers identifiers) {
|
||||||
|
this.dbSystem = dbSystem;
|
||||||
|
this.identifiers = identifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canAccess(Request request) {
|
||||||
|
return request.getUser()
|
||||||
|
.map(user -> user.hasPermission(WebPermission.PAGE_NETWORK_PLUGIN_HISTORY)
|
||||||
|
|| user.hasPermission(WebPermission.PAGE_SERVER_PLUGIN_HISTORY))
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Operation(
|
||||||
|
description = "Get plugin history for a server since installation of Plan.",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON,
|
||||||
|
schema = @Schema(implementation = PluginHistoryDto.class))),
|
||||||
|
},
|
||||||
|
parameters = @Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for (optional)", examples = {
|
||||||
|
@ExampleObject("Server 1"),
|
||||||
|
@ExampleObject("1"),
|
||||||
|
@ExampleObject("1fb39d2a-eb82-4868-b245-1fad17d823b3"),
|
||||||
|
}),
|
||||||
|
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
|
||||||
|
)
|
||||||
|
@GET
|
||||||
|
public Optional<Response> resolve(@Untrusted Request request) {
|
||||||
|
return Optional.of(getResponse(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response getResponse(@Untrusted Request request) {
|
||||||
|
ServerUUID serverUUID = identifiers.getServerUUID(request); // Can throw BadRequestException
|
||||||
|
List<PluginHistoryMetadata> history = dbSystem.getDatabase().query(PluginMetadataQueries.getPluginHistory(serverUUID));
|
||||||
|
return Response.builder()
|
||||||
|
.setMimeType(MimeType.JSON)
|
||||||
|
.setJSONContent(new PluginHistoryDto(history))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package com.djrapitops.plan.gathering;
|
package com.djrapitops.plan.gathering;
|
||||||
|
|
||||||
|
import com.djrapitops.plan.gathering.domain.PluginMetadata;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -64,4 +66,8 @@ public interface ServerSensor<W> {
|
||||||
default boolean usingRedisBungee() {
|
default boolean usingRedisBungee() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default List<PluginMetadata> getInstalledPlugins() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Player Analytics (Plan).
|
||||||
|
*
|
||||||
|
* Plan is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Plan is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.djrapitops.plan.gathering.domain;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a plugin that is installed on a server.
|
||||||
|
*
|
||||||
|
* @author AuroraLS3
|
||||||
|
*/
|
||||||
|
public class PluginMetadata {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final String version;
|
||||||
|
|
||||||
|
public PluginMetadata(String name, String version) {
|
||||||
|
this.name = name;
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
PluginMetadata that = (PluginMetadata) o;
|
||||||
|
return Objects.equals(getName(), that.getName()) && Objects.equals(getVersion(), that.getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(getName(), getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "PluginMetadata{" +
|
||||||
|
"name='" + name + '\'' +
|
||||||
|
", version='" + version + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Player Analytics (Plan).
|
||||||
|
*
|
||||||
|
* Plan is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Plan is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.djrapitops.plan.gathering.timed;
|
||||||
|
|
||||||
|
import com.djrapitops.plan.PlanSystem;
|
||||||
|
import com.djrapitops.plan.TaskSystem;
|
||||||
|
import com.djrapitops.plan.gathering.ServerSensor;
|
||||||
|
import com.djrapitops.plan.gathering.domain.PluginMetadata;
|
||||||
|
import com.djrapitops.plan.identification.ServerInfo;
|
||||||
|
import com.djrapitops.plan.identification.ServerUUID;
|
||||||
|
import com.djrapitops.plan.storage.database.DBSystem;
|
||||||
|
import com.djrapitops.plan.storage.database.queries.objects.PluginMetadataQueries;
|
||||||
|
import com.djrapitops.plan.storage.database.sql.tables.PluginVersionTable;
|
||||||
|
import com.djrapitops.plan.storage.database.transactions.events.StorePluginVersionsTransaction;
|
||||||
|
import net.playeranalytics.plugin.scheduling.RunnableFactory;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gathers information about plugins that have been installed and their version history.
|
||||||
|
*
|
||||||
|
* @author AuroraLS3
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class InstalledPluginGatheringTask extends TaskSystem.Task {
|
||||||
|
|
||||||
|
private final ServerSensor<?> serverSensor;
|
||||||
|
private final ServerInfo serverInfo;
|
||||||
|
private final DBSystem dbSystem;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public InstalledPluginGatheringTask(ServerSensor<?> serverSensor, ServerInfo serverInfo, DBSystem dbSystem) {
|
||||||
|
this.serverSensor = serverSensor;
|
||||||
|
this.serverInfo = serverInfo;
|
||||||
|
this.dbSystem = dbSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void register(RunnableFactory runnableFactory) {
|
||||||
|
runnableFactory.create(this)
|
||||||
|
.runTaskLater(20, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
List<PluginMetadata> installedPlugins = serverSensor.getInstalledPlugins();
|
||||||
|
|
||||||
|
ServerUUID serverUUID = serverInfo.getServerUUID();
|
||||||
|
List<PluginMetadata> previouslyInstalledPlugins = dbSystem.getDatabase()
|
||||||
|
.query(PluginMetadataQueries.getInstalledPlugins(serverUUID));
|
||||||
|
|
||||||
|
List<PluginMetadata> newPlugins = new ArrayList<>();
|
||||||
|
List<PluginMetadata> updatedPlugins = new ArrayList<>();
|
||||||
|
|
||||||
|
Set<String> installedPluginNames = new HashSet<>();
|
||||||
|
for (PluginMetadata installedPlugin : installedPlugins) {
|
||||||
|
installedPluginNames.add(installedPlugin.getName());
|
||||||
|
|
||||||
|
Optional<PluginMetadata> match = previouslyInstalledPlugins.stream()
|
||||||
|
.filter(plugin -> plugin.getName().equals(installedPlugin.getName()))
|
||||||
|
.findFirst();
|
||||||
|
if (match.isEmpty()) {
|
||||||
|
// New plugins are installedPlugins missing from previous list
|
||||||
|
newPlugins.add(installedPlugin);
|
||||||
|
} else {
|
||||||
|
PluginMetadata previousVersion = match.get();
|
||||||
|
String installedVersion = StringUtils.truncate(installedPlugin.getVersion(), PluginVersionTable.MAX_VERSION_LENGTH);
|
||||||
|
if (!installedVersion.equals(previousVersion.getVersion())) {
|
||||||
|
// Updated plugins are plugins in the list with different version
|
||||||
|
updatedPlugins.add(installedPlugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed plugins are previously installed plugins missing from installed list
|
||||||
|
List<PluginMetadata> removedPlugins = previouslyInstalledPlugins.stream()
|
||||||
|
.map(PluginMetadata::getName)
|
||||||
|
.filter(pluginName -> !installedPluginNames.contains(pluginName))
|
||||||
|
// Uninstalled plugin version is marked as null
|
||||||
|
.map(pluginName -> new PluginMetadata(pluginName, null))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
long enableTime = PlanSystem.getServerEnableTime();
|
||||||
|
List<PluginMetadata> pluginChangeList = new ArrayList<>();
|
||||||
|
pluginChangeList.addAll(newPlugins);
|
||||||
|
pluginChangeList.addAll(updatedPlugins);
|
||||||
|
pluginChangeList.addAll(removedPlugins);
|
||||||
|
|
||||||
|
dbSystem.getDatabase().executeTransaction(new StorePluginVersionsTransaction(enableTime, serverUUID, pluginChangeList));
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import com.djrapitops.plan.settings.config.ConfigNode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a change made to the config structure.
|
* Represents a change made to the config structure.
|
||||||
|
@ -103,6 +104,8 @@ public interface ConfigChange {
|
||||||
public boolean hasBeenApplied(Config config) {
|
public boolean hasBeenApplied(Config config) {
|
||||||
return config.getNode(oldPath)
|
return config.getNode(oldPath)
|
||||||
.map(ConfigNode::getString)
|
.map(ConfigNode::getString)
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(Predicate.not(String::isEmpty))
|
||||||
.isEmpty()
|
.isEmpty()
|
||||||
&& config.getNode(newPath).isPresent();
|
&& config.getNode(newPath).isPresent();
|
||||||
}
|
}
|
||||||
|
|
|
@ -270,6 +270,13 @@ public enum HtmlLang implements Lang {
|
||||||
LABEL_SERVER_SELECTOR("html.label.serverSelector", "Server selector"),
|
LABEL_SERVER_SELECTOR("html.label.serverSelector", "Server selector"),
|
||||||
LABEL_APPLY("html.label.apply", "Apply"),
|
LABEL_APPLY("html.label.apply", "Apply"),
|
||||||
LABEL_POSSIBLY_OFFLINE("html.label.serverPossiblyOffline", "Possibly offline"),
|
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_VISIBLE_COLUMNS("html.label.table.visibleColumns", "Visible columns"),
|
||||||
LABEL_TABLE_SHOW_N_OF_M("html.label.table.showNofM", "Showing {{n}} of {{m}} entries"),
|
LABEL_TABLE_SHOW_N_OF_M("html.label.table.showNofM", "Showing {{n}} of {{m}} entries"),
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Player Analytics (Plan).
|
||||||
|
*
|
||||||
|
* Plan is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Plan is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.djrapitops.plan.storage.database.queries.objects;
|
||||||
|
|
||||||
|
import com.djrapitops.plan.delivery.domain.PluginHistoryMetadata;
|
||||||
|
import com.djrapitops.plan.gathering.domain.PluginMetadata;
|
||||||
|
import com.djrapitops.plan.identification.ServerUUID;
|
||||||
|
import com.djrapitops.plan.storage.database.queries.Query;
|
||||||
|
import com.djrapitops.plan.storage.database.queries.QueryStatement;
|
||||||
|
import com.djrapitops.plan.storage.database.sql.tables.PluginVersionTable;
|
||||||
|
import com.djrapitops.plan.storage.database.sql.tables.ServerTable;
|
||||||
|
import org.intellij.lang.annotations.Language;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries to the {@link com.djrapitops.plan.storage.database.sql.tables.PluginVersionTable} go here.
|
||||||
|
*
|
||||||
|
* @author AuroraLS3
|
||||||
|
*/
|
||||||
|
public class PluginMetadataQueries {
|
||||||
|
|
||||||
|
private PluginMetadataQueries() {
|
||||||
|
/* static method class */
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Query<List<PluginMetadata>> getInstalledPlugins(ServerUUID serverUUID) {
|
||||||
|
@Language("SQL")
|
||||||
|
String sql = SELECT + "*" + FROM + PluginVersionTable.TABLE_NAME +
|
||||||
|
WHERE + PluginVersionTable.SERVER_ID + "=" + ServerTable.SELECT_SERVER_ID +
|
||||||
|
ORDER_BY + PluginVersionTable.MODIFIED + " DESC";
|
||||||
|
return new QueryStatement<>(sql, 100) {
|
||||||
|
@Override
|
||||||
|
public void prepare(PreparedStatement statement) throws SQLException {
|
||||||
|
statement.setString(1, serverUUID.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PluginMetadata> processResults(ResultSet set) throws SQLException {
|
||||||
|
Set<String> foundPlugins = new HashSet<>();
|
||||||
|
List<PluginMetadata> installedPlugins = new ArrayList<>();
|
||||||
|
while (set.next()) {
|
||||||
|
String pluginName = set.getString(PluginVersionTable.PLUGIN_NAME);
|
||||||
|
|
||||||
|
// Only keep the latest information
|
||||||
|
if (foundPlugins.contains(pluginName)) continue;
|
||||||
|
foundPlugins.add(pluginName);
|
||||||
|
|
||||||
|
String version = set.getString(PluginVersionTable.VERSION);
|
||||||
|
if (!set.wasNull()) { // If version is null the plugin is marked as uninstalled.
|
||||||
|
installedPlugins.add(new PluginMetadata(pluginName, version));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return installedPlugins;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Query<List<PluginHistoryMetadata>> getPluginHistory(ServerUUID serverUUID) {
|
||||||
|
@Language("SQL")
|
||||||
|
String sql = SELECT + "*" + FROM + PluginVersionTable.TABLE_NAME +
|
||||||
|
WHERE + PluginVersionTable.SERVER_ID + "=" + ServerTable.SELECT_SERVER_ID +
|
||||||
|
ORDER_BY + PluginVersionTable.MODIFIED + " DESC, " + PluginVersionTable.PLUGIN_NAME;
|
||||||
|
return db -> db.queryList(sql, PluginMetadataQueries::extractHistoryMetadata, serverUUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static PluginHistoryMetadata extractHistoryMetadata(ResultSet row) throws SQLException {
|
||||||
|
String name = row.getString(PluginVersionTable.PLUGIN_NAME);
|
||||||
|
String version = row.getString(PluginVersionTable.VERSION);
|
||||||
|
if (row.wasNull()) version = null;
|
||||||
|
long modified = row.getLong(PluginVersionTable.MODIFIED);
|
||||||
|
return new PluginHistoryMetadata(name, version, modified);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Player Analytics (Plan).
|
||||||
|
*
|
||||||
|
* Plan is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Plan is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.djrapitops.plan.storage.database.sql.tables;
|
||||||
|
|
||||||
|
import com.djrapitops.plan.storage.database.DBType;
|
||||||
|
import com.djrapitops.plan.storage.database.sql.building.CreateTableBuilder;
|
||||||
|
import com.djrapitops.plan.storage.database.sql.building.Sql;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents plan_plugin_versions table.
|
||||||
|
* <p>
|
||||||
|
* Keeps track of plugin version history.
|
||||||
|
*
|
||||||
|
* @author AuroraLS3
|
||||||
|
*/
|
||||||
|
public class PluginVersionTable {
|
||||||
|
|
||||||
|
public static final String TABLE_NAME = "plan_plugin_versions";
|
||||||
|
|
||||||
|
public static final String ID = "id";
|
||||||
|
public static final String SERVER_ID = "server_id";
|
||||||
|
public static final String PLUGIN_NAME = "plugin_name";
|
||||||
|
public static final String VERSION = "version";
|
||||||
|
public static final String MODIFIED = "modified";
|
||||||
|
|
||||||
|
public static final int MAX_NAME_LENGTH = 100;
|
||||||
|
public static final int MAX_VERSION_LENGTH = 255;
|
||||||
|
|
||||||
|
public static final String INSERT_STATEMENT = "INSERT INTO " + TABLE_NAME + " ("
|
||||||
|
+ SERVER_ID + ','
|
||||||
|
+ PLUGIN_NAME + ','
|
||||||
|
+ VERSION + ','
|
||||||
|
+ MODIFIED
|
||||||
|
+ ") VALUES (" + ServerTable.SELECT_SERVER_ID + ", ?, ?, ?)";
|
||||||
|
|
||||||
|
private PluginVersionTable() {
|
||||||
|
/* Static information class */
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String createTableSQL(DBType dbType) {
|
||||||
|
return CreateTableBuilder.create(TABLE_NAME, dbType)
|
||||||
|
.column(ID, Sql.INT).primaryKey()
|
||||||
|
.column(SERVER_ID, Sql.INT).notNull()
|
||||||
|
.column(PLUGIN_NAME, Sql.varchar(100)).notNull()
|
||||||
|
.column(VERSION, Sql.varchar(255))
|
||||||
|
.column(MODIFIED, Sql.LONG).notNull().defaultValue("0")
|
||||||
|
.foreignKey(SERVER_ID, ServerTable.TABLE_NAME, ServerTable.ID)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Player Analytics (Plan).
|
||||||
|
*
|
||||||
|
* Plan is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Plan is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.djrapitops.plan.storage.database.transactions.events;
|
||||||
|
|
||||||
|
import com.djrapitops.plan.gathering.domain.PluginMetadata;
|
||||||
|
import com.djrapitops.plan.identification.ServerUUID;
|
||||||
|
import com.djrapitops.plan.storage.database.sql.tables.PluginVersionTable;
|
||||||
|
import com.djrapitops.plan.storage.database.transactions.ExecBatchStatement;
|
||||||
|
import com.djrapitops.plan.storage.database.transactions.Transaction;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores changes to the plugin list found during enable.
|
||||||
|
*
|
||||||
|
* @author AuroraLS3
|
||||||
|
*/
|
||||||
|
public class StorePluginVersionsTransaction extends Transaction {
|
||||||
|
|
||||||
|
private final long time;
|
||||||
|
private final ServerUUID serverUUID;
|
||||||
|
private final List<PluginMetadata> changeList;
|
||||||
|
|
||||||
|
public StorePluginVersionsTransaction(long time, ServerUUID serverUUID, List<PluginMetadata> changeList) {
|
||||||
|
this.time = time;
|
||||||
|
this.serverUUID = serverUUID;
|
||||||
|
this.changeList = changeList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void performOperations() {
|
||||||
|
execute(new ExecBatchStatement(PluginVersionTable.INSERT_STATEMENT) {
|
||||||
|
@Override
|
||||||
|
public void prepare(PreparedStatement statement) throws SQLException {
|
||||||
|
for (PluginMetadata plugin : changeList) {
|
||||||
|
statement.setString(1, serverUUID.toString());
|
||||||
|
statement.setString(2, StringUtils.truncate(plugin.getName(), PluginVersionTable.MAX_NAME_LENGTH));
|
||||||
|
if (plugin.getVersion() == null) {
|
||||||
|
statement.setNull(3, Types.VARCHAR);
|
||||||
|
} else {
|
||||||
|
statement.setString(3, StringUtils.truncate(plugin.getVersion(), PluginVersionTable.MAX_VERSION_LENGTH));
|
||||||
|
}
|
||||||
|
statement.setLong(4, time);
|
||||||
|
statement.addBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visible for testing
|
||||||
|
public List<PluginMetadata> getChangeList() {
|
||||||
|
return changeList;
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,6 +56,7 @@ public class CreateTablesTransaction extends OperationCriticalTransaction {
|
||||||
// Ensure plan_security has id column
|
// Ensure plan_security has id column
|
||||||
executeOther(new SecurityTableIdPatch());
|
executeOther(new SecurityTableIdPatch());
|
||||||
execute(WebUserPreferencesTable.createTableSQL(dbType));
|
execute(WebUserPreferencesTable.createTableSQL(dbType));
|
||||||
|
execute(PluginVersionTable.createTableSQL(dbType));
|
||||||
|
|
||||||
// DataExtension tables
|
// DataExtension tables
|
||||||
execute(ExtensionIconTable.createTableSQL(dbType));
|
execute(ExtensionIconTable.createTableSQL(dbType));
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU 使用率"
|
cpuUsage: "CPU 使用率"
|
||||||
currentPlayerbase: "当前玩家数"
|
currentPlayerbase: "当前玩家数"
|
||||||
currentUptime: "正常运行时间"
|
currentUptime: "正常运行时间"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "按天查看"
|
dayByDay: "按天查看"
|
||||||
dayOfweek: "星期"
|
dayOfweek: "星期"
|
||||||
deadliestWeapon: "最致命的 PVP 武器"
|
deadliestWeapon: "最致命的 PVP 武器"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "信息"
|
information: "信息"
|
||||||
insights: "洞察"
|
insights: "洞察"
|
||||||
insights30days: "30 天分析"
|
insights30days: "30 天分析"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "偶尔上线"
|
irregular: "偶尔上线"
|
||||||
joinAddress: "加入地址"
|
joinAddress: "加入地址"
|
||||||
joinAddresses: "加入地址"
|
joinAddresses: "加入地址"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "被生物击杀数"
|
mobDeaths: "被生物击杀数"
|
||||||
mobKdr: "生物 KDR"
|
mobKdr: "生物 KDR"
|
||||||
mobKills: "生物击杀数"
|
mobKills: "生物击杀数"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "最常玩的游戏模式"
|
mostActiveGamemode: "最常玩的游戏模式"
|
||||||
mostPlayedWorld: "玩的最多的世界"
|
mostPlayedWorld: "玩的最多的世界"
|
||||||
name: "名称"
|
name: "名称"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "在线玩家(当前)"
|
playersOnlineNow: "在线玩家(当前)"
|
||||||
playersOnlineOverview: "在线活动总览"
|
playersOnlineOverview: "在线活动总览"
|
||||||
playtime: "游玩时间"
|
playtime: "游玩时间"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "插件"
|
plugins: "插件"
|
||||||
pluginsOverview: "插件总览"
|
pluginsOverview: "插件总览"
|
||||||
punchcard: "打卡"
|
punchcard: "打卡"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "趋势"
|
trend: "趋势"
|
||||||
trends30days: "30 天趋势"
|
trends30days: "30 天趋势"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "独立玩家"
|
uniquePlayers: "独立玩家"
|
||||||
uniquePlayers7days: "独立玩家(7天)"
|
uniquePlayers7days: "独立玩家(7天)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "百分比"
|
percentage: "百分比"
|
||||||
playerCount: "玩家数量"
|
playerCount: "玩家数量"
|
||||||
users: "管理用户"
|
users: "管理用户"
|
||||||
|
version: "Version"
|
||||||
veryActive: "非常活跃"
|
veryActive: "非常活跃"
|
||||||
weekComparison: "每周对比"
|
weekComparison: "每周对比"
|
||||||
weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'"
|
weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU Usage"
|
cpuUsage: "CPU Usage"
|
||||||
currentPlayerbase: "Aktuální základna hráčů"
|
currentPlayerbase: "Aktuální základna hráčů"
|
||||||
currentUptime: "Aktuální doba zapnutí"
|
currentUptime: "Aktuální doba zapnutí"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Den po dni"
|
dayByDay: "Den po dni"
|
||||||
dayOfweek: "Den týdne"
|
dayOfweek: "Den týdne"
|
||||||
deadliestWeapon: "Nejsmrtelnější PvP zbraň"
|
deadliestWeapon: "Nejsmrtelnější PvP zbraň"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "INFORMACE"
|
information: "INFORMACE"
|
||||||
insights: "Postřehy"
|
insights: "Postřehy"
|
||||||
insights30days: "Postřehy za 30 dní"
|
insights30days: "Postřehy za 30 dní"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Nepravidelný"
|
irregular: "Nepravidelný"
|
||||||
joinAddress: "Adresa Připojení"
|
joinAddress: "Adresa Připojení"
|
||||||
joinAddresses: "IP Připojení"
|
joinAddresses: "IP Připojení"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Smrti způsobené moby"
|
mobDeaths: "Smrti způsobené moby"
|
||||||
mobKdr: "Mob KDR"
|
mobKdr: "Mob KDR"
|
||||||
mobKills: "Zabití mobové"
|
mobKills: "Zabití mobové"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "Nejvíce aktivní mód"
|
mostActiveGamemode: "Nejvíce aktivní mód"
|
||||||
mostPlayedWorld: "Nejvíc hraný svět"
|
mostPlayedWorld: "Nejvíc hraný svět"
|
||||||
name: "Jméno"
|
name: "Jméno"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Hráči online (Nyní)"
|
playersOnlineNow: "Hráči online (Nyní)"
|
||||||
playersOnlineOverview: "Přehled online aktivity"
|
playersOnlineOverview: "Přehled online aktivity"
|
||||||
playtime: "Herní čas"
|
playtime: "Herní čas"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Pluginy"
|
plugins: "Pluginy"
|
||||||
pluginsOverview: "Přehled pluginů"
|
pluginsOverview: "Přehled pluginů"
|
||||||
punchcard: "Štítky"
|
punchcard: "Štítky"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Trend"
|
trend: "Trend"
|
||||||
trends30days: "Trendy za 30 dní"
|
trends30days: "Trendy za 30 dní"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Unikátní hráči"
|
uniquePlayers: "Unikátní hráči"
|
||||||
uniquePlayers7days: "Unikátních hráčů (7 dní)"
|
uniquePlayers7days: "Unikátních hráčů (7 dní)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Procento"
|
percentage: "Procento"
|
||||||
playerCount: "Počet hráčů"
|
playerCount: "Počet hráčů"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Velmi aktivní"
|
veryActive: "Velmi aktivní"
|
||||||
weekComparison: "Týdenní srovnání"
|
weekComparison: "Týdenní srovnání"
|
||||||
weekdays: "'Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek', 'Sobota', 'Neděle'"
|
weekdays: "'Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek', 'Sobota', 'Neděle'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU Usage"
|
cpuUsage: "CPU Usage"
|
||||||
currentPlayerbase: "Aktuelle Spielerbasis"
|
currentPlayerbase: "Aktuelle Spielerbasis"
|
||||||
currentUptime: "Current Uptime"
|
currentUptime: "Current Uptime"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Tag für Tag"
|
dayByDay: "Tag für Tag"
|
||||||
dayOfweek: "Tag der Woche"
|
dayOfweek: "Tag der Woche"
|
||||||
deadliestWeapon: "Tödlichste PvP Waffe"
|
deadliestWeapon: "Tödlichste PvP Waffe"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "INFORMATION"
|
information: "INFORMATION"
|
||||||
insights: "Insights"
|
insights: "Insights"
|
||||||
insights30days: "Insights for 30 days"
|
insights30days: "Insights for 30 days"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Unregelmäßig"
|
irregular: "Unregelmäßig"
|
||||||
joinAddress: "Join Address"
|
joinAddress: "Join Address"
|
||||||
joinAddresses: "Join Addresses"
|
joinAddresses: "Join Addresses"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Tode durch Mobs"
|
mobDeaths: "Tode durch Mobs"
|
||||||
mobKdr: "Mob KDR"
|
mobKdr: "Mob KDR"
|
||||||
mobKills: "Mob Kills"
|
mobKills: "Mob Kills"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "Meist genutzter Spielmodus"
|
mostActiveGamemode: "Meist genutzter Spielmodus"
|
||||||
mostPlayedWorld: "Meist gespielte Welt"
|
mostPlayedWorld: "Meist gespielte Welt"
|
||||||
name: "Name"
|
name: "Name"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Players Online (Now)"
|
playersOnlineNow: "Players Online (Now)"
|
||||||
playersOnlineOverview: "Online Aktivitätsübersicht"
|
playersOnlineOverview: "Online Aktivitätsübersicht"
|
||||||
playtime: "Spielzeit"
|
playtime: "Spielzeit"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Plugins"
|
plugins: "Plugins"
|
||||||
pluginsOverview: "Plugins Overview"
|
pluginsOverview: "Plugins Overview"
|
||||||
punchcard: "Lochkarte"
|
punchcard: "Lochkarte"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Trend"
|
trend: "Trend"
|
||||||
trends30days: "Trends für 30 Tage"
|
trends30days: "Trends für 30 Tage"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Einzigartige Spieler"
|
uniquePlayers: "Einzigartige Spieler"
|
||||||
uniquePlayers7days: "Unique Players (7 days)"
|
uniquePlayers7days: "Unique Players (7 days)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Percentage"
|
percentage: "Percentage"
|
||||||
playerCount: "Player Count"
|
playerCount: "Player Count"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Sehr aktiv"
|
veryActive: "Sehr aktiv"
|
||||||
weekComparison: "Wochenvergleich"
|
weekComparison: "Wochenvergleich"
|
||||||
weekdays: "'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'"
|
weekdays: "'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU Usage"
|
cpuUsage: "CPU Usage"
|
||||||
currentPlayerbase: "Current Playerbase"
|
currentPlayerbase: "Current Playerbase"
|
||||||
currentUptime: "Current Uptime"
|
currentUptime: "Current Uptime"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Day by Day"
|
dayByDay: "Day by Day"
|
||||||
dayOfweek: "Day of the Week"
|
dayOfweek: "Day of the Week"
|
||||||
deadliestWeapon: "Deadliest PvP Weapon"
|
deadliestWeapon: "Deadliest PvP Weapon"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "INFORMATION"
|
information: "INFORMATION"
|
||||||
insights: "Insights"
|
insights: "Insights"
|
||||||
insights30days: "Insights for 30 days"
|
insights30days: "Insights for 30 days"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Irregular"
|
irregular: "Irregular"
|
||||||
joinAddress: "Join Address"
|
joinAddress: "Join Address"
|
||||||
joinAddresses: "Join Addresses"
|
joinAddresses: "Join Addresses"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Mob caused Deaths"
|
mobDeaths: "Mob caused Deaths"
|
||||||
mobKdr: "Mob KDR"
|
mobKdr: "Mob KDR"
|
||||||
mobKills: "Mob Kills"
|
mobKills: "Mob Kills"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "Most Active Gamemode"
|
mostActiveGamemode: "Most Active Gamemode"
|
||||||
mostPlayedWorld: "Most played World"
|
mostPlayedWorld: "Most played World"
|
||||||
name: "Name"
|
name: "Name"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Players Online (Now)"
|
playersOnlineNow: "Players Online (Now)"
|
||||||
playersOnlineOverview: "Online Activity Overview"
|
playersOnlineOverview: "Online Activity Overview"
|
||||||
playtime: "Playtime"
|
playtime: "Playtime"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Plugins"
|
plugins: "Plugins"
|
||||||
pluginsOverview: "Plugins Overview"
|
pluginsOverview: "Plugins Overview"
|
||||||
punchcard: "Punchcard"
|
punchcard: "Punchcard"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Trend"
|
trend: "Trend"
|
||||||
trends30days: "Trends for 30 days"
|
trends30days: "Trends for 30 days"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Unique Players"
|
uniquePlayers: "Unique Players"
|
||||||
uniquePlayers7days: "Unique Players (7 days)"
|
uniquePlayers7days: "Unique Players (7 days)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Percentage"
|
percentage: "Percentage"
|
||||||
playerCount: "Player Count"
|
playerCount: "Player Count"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Very Active"
|
veryActive: "Very Active"
|
||||||
weekComparison: "Week Comparison"
|
weekComparison: "Week Comparison"
|
||||||
weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"
|
weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU Usage"
|
cpuUsage: "CPU Usage"
|
||||||
currentPlayerbase: "base del jugador actual"
|
currentPlayerbase: "base del jugador actual"
|
||||||
currentUptime: "Current Uptime"
|
currentUptime: "Current Uptime"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Día a día"
|
dayByDay: "Día a día"
|
||||||
dayOfweek: "Dia de la semana"
|
dayOfweek: "Dia de la semana"
|
||||||
deadliestWeapon: "Arma PvP más mortal"
|
deadliestWeapon: "Arma PvP más mortal"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "INFORMACIÓN"
|
information: "INFORMACIÓN"
|
||||||
insights: "Insights"
|
insights: "Insights"
|
||||||
insights30days: "Ideas por 30 días"
|
insights30days: "Ideas por 30 días"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Irregular"
|
irregular: "Irregular"
|
||||||
joinAddress: "Join Address"
|
joinAddress: "Join Address"
|
||||||
joinAddresses: "Direcciones de entrada"
|
joinAddresses: "Direcciones de entrada"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Muertes causadas por mobs"
|
mobDeaths: "Muertes causadas por mobs"
|
||||||
mobKdr: "KDR de mobs"
|
mobKdr: "KDR de mobs"
|
||||||
mobKills: "Asesinatos de mobs"
|
mobKills: "Asesinatos de mobs"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "Modo de juego mas activo"
|
mostActiveGamemode: "Modo de juego mas activo"
|
||||||
mostPlayedWorld: "Mundo más jugado"
|
mostPlayedWorld: "Mundo más jugado"
|
||||||
name: "Nombre"
|
name: "Nombre"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Players Online (Now)"
|
playersOnlineNow: "Players Online (Now)"
|
||||||
playersOnlineOverview: "Vista general de la actividad online"
|
playersOnlineOverview: "Vista general de la actividad online"
|
||||||
playtime: "Tiempo de juego"
|
playtime: "Tiempo de juego"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Plugins"
|
plugins: "Plugins"
|
||||||
pluginsOverview: "Plugins Overview"
|
pluginsOverview: "Plugins Overview"
|
||||||
punchcard: "Tarjeta"
|
punchcard: "Tarjeta"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Tendencia"
|
trend: "Tendencia"
|
||||||
trends30days: "Tendencias de 30 días"
|
trends30days: "Tendencias de 30 días"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Jugadores únicos"
|
uniquePlayers: "Jugadores únicos"
|
||||||
uniquePlayers7days: "Unique Players (7 days)"
|
uniquePlayers7days: "Unique Players (7 days)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Percentage"
|
percentage: "Percentage"
|
||||||
playerCount: "Player Count"
|
playerCount: "Player Count"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Muy activo"
|
veryActive: "Muy activo"
|
||||||
weekComparison: "Comparación semanal"
|
weekComparison: "Comparación semanal"
|
||||||
weekdays: "'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'"
|
weekdays: "'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "Suorittimen käyttö"
|
cpuUsage: "Suorittimen käyttö"
|
||||||
currentPlayerbase: "Nykyiset pelaajat"
|
currentPlayerbase: "Nykyiset pelaajat"
|
||||||
currentUptime: "Käynnissäoloaika"
|
currentUptime: "Käynnissäoloaika"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Päivittäinen katsaus"
|
dayByDay: "Päivittäinen katsaus"
|
||||||
dayOfweek: "Viikonpäivä"
|
dayOfweek: "Viikonpäivä"
|
||||||
deadliestWeapon: "Tappavin PvP Ase"
|
deadliestWeapon: "Tappavin PvP Ase"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "TIETOJA"
|
information: "TIETOJA"
|
||||||
insights: "Katsaukset"
|
insights: "Katsaukset"
|
||||||
insights30days: "Katsauksia 30 päivälle"
|
insights30days: "Katsauksia 30 päivälle"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Epäsäännöllinen"
|
irregular: "Epäsäännöllinen"
|
||||||
joinAddress: "Liittymisosoite"
|
joinAddress: "Liittymisosoite"
|
||||||
joinAddresses: "Liittymisosoitteet"
|
joinAddresses: "Liittymisosoitteet"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Otusten aiheuttamat Kuolemat"
|
mobDeaths: "Otusten aiheuttamat Kuolemat"
|
||||||
mobKdr: "Otus-Tapposuhde"
|
mobKdr: "Otus-Tapposuhde"
|
||||||
mobKills: "Tapetut Otukset"
|
mobKills: "Tapetut Otukset"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "Aktiivisin pelitila"
|
mostActiveGamemode: "Aktiivisin pelitila"
|
||||||
mostPlayedWorld: "Eniten pelattu maailma"
|
mostPlayedWorld: "Eniten pelattu maailma"
|
||||||
name: "Nimi"
|
name: "Nimi"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Pelaajia paikalla (Nyt)"
|
playersOnlineNow: "Pelaajia paikalla (Nyt)"
|
||||||
playersOnlineOverview: "Yhteenveto Paikallaolosta"
|
playersOnlineOverview: "Yhteenveto Paikallaolosta"
|
||||||
playtime: "Peliaika"
|
playtime: "Peliaika"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Lisäosat"
|
plugins: "Lisäosat"
|
||||||
pluginsOverview: "Lisäosien Yhteenveto"
|
pluginsOverview: "Lisäosien Yhteenveto"
|
||||||
punchcard: "Reikäkortti"
|
punchcard: "Reikäkortti"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Suunta"
|
trend: "Suunta"
|
||||||
trends30days: "Suunnat 30 päivälle"
|
trends30days: "Suunnat 30 päivälle"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Uniikkeja pelaajia"
|
uniquePlayers: "Uniikkeja pelaajia"
|
||||||
uniquePlayers7days: "Uniikkeja pelaajia (7 päivää)"
|
uniquePlayers7days: "Uniikkeja pelaajia (7 päivää)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Prosentti"
|
percentage: "Prosentti"
|
||||||
playerCount: "Pelaajamäärä"
|
playerCount: "Pelaajamäärä"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Todella Aktiivinen"
|
veryActive: "Todella Aktiivinen"
|
||||||
weekComparison: "Viikkojen vertaus"
|
weekComparison: "Viikkojen vertaus"
|
||||||
weekdays: "'Maanantai', 'Tiistai', 'Keskiviikko', 'Torstai', 'Perjantai', 'Lauantai', 'Sunnuntai'"
|
weekdays: "'Maanantai', 'Tiistai', 'Keskiviikko', 'Torstai', 'Perjantai', 'Lauantai', 'Sunnuntai'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU Usage"
|
cpuUsage: "CPU Usage"
|
||||||
currentPlayerbase: "Base de Joueurs actuelle"
|
currentPlayerbase: "Base de Joueurs actuelle"
|
||||||
currentUptime: "Current Uptime"
|
currentUptime: "Current Uptime"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Jour par Jour"
|
dayByDay: "Jour par Jour"
|
||||||
dayOfweek: "Jour de la Semaine"
|
dayOfweek: "Jour de la Semaine"
|
||||||
deadliestWeapon: "1ère Arme de Combat (la plus mortelle)"
|
deadliestWeapon: "1ère Arme de Combat (la plus mortelle)"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "INFORMATIONS"
|
information: "INFORMATIONS"
|
||||||
insights: "Insights"
|
insights: "Insights"
|
||||||
insights30days: "Perspectives sur 30 jours"
|
insights30days: "Perspectives sur 30 jours"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Irrégulier"
|
irregular: "Irrégulier"
|
||||||
joinAddress: "Join Address"
|
joinAddress: "Join Address"
|
||||||
joinAddresses: "Adresses de Connexion"
|
joinAddresses: "Adresses de Connexion"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Morts causées par un Mob"
|
mobDeaths: "Morts causées par un Mob"
|
||||||
mobKdr: "Ratio - Kills / Morts de Mobs -"
|
mobKdr: "Ratio - Kills / Morts de Mobs -"
|
||||||
mobKills: "Kills de Mobs"
|
mobKills: "Kills de Mobs"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "Mode de Jeu le plus utilisé"
|
mostActiveGamemode: "Mode de Jeu le plus utilisé"
|
||||||
mostPlayedWorld: "Monde le plus Fréquenté"
|
mostPlayedWorld: "Monde le plus Fréquenté"
|
||||||
name: "Nom"
|
name: "Nom"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Players Online (Now)"
|
playersOnlineNow: "Players Online (Now)"
|
||||||
playersOnlineOverview: "Aperçu de l'Activité en Ligne"
|
playersOnlineOverview: "Aperçu de l'Activité en Ligne"
|
||||||
playtime: "Temps de Jeu"
|
playtime: "Temps de Jeu"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Plugins"
|
plugins: "Plugins"
|
||||||
pluginsOverview: "Plugins Overview"
|
pluginsOverview: "Plugins Overview"
|
||||||
punchcard: "Carte Perforée"
|
punchcard: "Carte Perforée"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Tendances"
|
trend: "Tendances"
|
||||||
trends30days: "Tendances sur 30 Jours"
|
trends30days: "Tendances sur 30 Jours"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Joueurs Uniques"
|
uniquePlayers: "Joueurs Uniques"
|
||||||
uniquePlayers7days: "Unique Players (7 days)"
|
uniquePlayers7days: "Unique Players (7 days)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Percentage"
|
percentage: "Percentage"
|
||||||
playerCount: "Player Count"
|
playerCount: "Player Count"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Très Actif"
|
veryActive: "Très Actif"
|
||||||
weekComparison: "Comparaison Hebdomadaire"
|
weekComparison: "Comparaison Hebdomadaire"
|
||||||
weekdays: "'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'"
|
weekdays: "'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU Usage"
|
cpuUsage: "CPU Usage"
|
||||||
currentPlayerbase: "Playerbase Corrente"
|
currentPlayerbase: "Playerbase Corrente"
|
||||||
currentUptime: "Current Uptime"
|
currentUptime: "Current Uptime"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Giorno Per Giorno"
|
dayByDay: "Giorno Per Giorno"
|
||||||
dayOfweek: "Giorno della Settimana"
|
dayOfweek: "Giorno della Settimana"
|
||||||
deadliestWeapon: "Arma PvP Preferita"
|
deadliestWeapon: "Arma PvP Preferita"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "INFORMAZIONE"
|
information: "INFORMAZIONE"
|
||||||
insights: "Insights"
|
insights: "Insights"
|
||||||
insights30days: "Grafico in 30 giorni"
|
insights30days: "Grafico in 30 giorni"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Irregolare"
|
irregular: "Irregolare"
|
||||||
joinAddress: "Join Address"
|
joinAddress: "Join Address"
|
||||||
joinAddresses: "Join Addresses"
|
joinAddresses: "Join Addresses"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Mob che hanno causato la Morte"
|
mobDeaths: "Mob che hanno causato la Morte"
|
||||||
mobKdr: "Mob KDR"
|
mobKdr: "Mob KDR"
|
||||||
mobKills: "Mob uccisi"
|
mobKills: "Mob uccisi"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "La Gamemode più attiva"
|
mostActiveGamemode: "La Gamemode più attiva"
|
||||||
mostPlayedWorld: "Mondo più giocato"
|
mostPlayedWorld: "Mondo più giocato"
|
||||||
name: "Nome"
|
name: "Nome"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Players Online (Now)"
|
playersOnlineNow: "Players Online (Now)"
|
||||||
playersOnlineOverview: "Panoramica delle attività online"
|
playersOnlineOverview: "Panoramica delle attività online"
|
||||||
playtime: "Gioco"
|
playtime: "Gioco"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Plugins"
|
plugins: "Plugins"
|
||||||
pluginsOverview: "Plugins Overview"
|
pluginsOverview: "Plugins Overview"
|
||||||
punchcard: "Presenza Settimanale"
|
punchcard: "Presenza Settimanale"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Tendenza"
|
trend: "Tendenza"
|
||||||
trends30days: "Tendenza per 30 giorni"
|
trends30days: "Tendenza per 30 giorni"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Giocatori unici"
|
uniquePlayers: "Giocatori unici"
|
||||||
uniquePlayers7days: "Unique Players (7 days)"
|
uniquePlayers7days: "Unique Players (7 days)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Percentage"
|
percentage: "Percentage"
|
||||||
playerCount: "Player Count"
|
playerCount: "Player Count"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Molto Attivo"
|
veryActive: "Molto Attivo"
|
||||||
weekComparison: "Confronto settimanale"
|
weekComparison: "Confronto settimanale"
|
||||||
weekdays: "'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica'"
|
weekdays: "'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU使用率"
|
cpuUsage: "CPU使用率"
|
||||||
currentPlayerbase: "ログインプレイヤー"
|
currentPlayerbase: "ログインプレイヤー"
|
||||||
currentUptime: "現状のアップタイム"
|
currentUptime: "現状のアップタイム"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "詳細情報"
|
dayByDay: "詳細情報"
|
||||||
dayOfweek: "曜日"
|
dayOfweek: "曜日"
|
||||||
deadliestWeapon: "最もPvPで使用されている武器"
|
deadliestWeapon: "最もPvPで使用されている武器"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "インフォメーション"
|
information: "インフォメーション"
|
||||||
insights: "Insights"
|
insights: "Insights"
|
||||||
insights30days: "1ヶ月のパンチボード"
|
insights30days: "1ヶ月のパンチボード"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "たまにログインしている"
|
irregular: "たまにログインしている"
|
||||||
joinAddress: "参加したサーバーのアドレス"
|
joinAddress: "参加したサーバーのアドレス"
|
||||||
joinAddresses: "参加したサーバーのアドレス"
|
joinAddresses: "参加したサーバーのアドレス"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Mobによって殺された回数"
|
mobDeaths: "Mobによって殺された回数"
|
||||||
mobKdr: "Mobに対してのKDR"
|
mobKdr: "Mobに対してのKDR"
|
||||||
mobKills: "Mobを殺した回数"
|
mobKills: "Mobを殺した回数"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "最も使用したゲームモード"
|
mostActiveGamemode: "最も使用したゲームモード"
|
||||||
mostPlayedWorld: "よくプレイしているワールド"
|
mostPlayedWorld: "よくプレイしているワールド"
|
||||||
name: "名前"
|
name: "名前"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "オンラインのプレイヤー(今)"
|
playersOnlineNow: "オンラインのプレイヤー(今)"
|
||||||
playersOnlineOverview: "接続状況の概要"
|
playersOnlineOverview: "接続状況の概要"
|
||||||
playtime: "プレイ時間"
|
playtime: "プレイ時間"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "プラグイン"
|
plugins: "プラグイン"
|
||||||
pluginsOverview: "プラグイン一覧"
|
pluginsOverview: "プラグイン一覧"
|
||||||
punchcard: "パンチカード"
|
punchcard: "パンチカード"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "増減"
|
trend: "増減"
|
||||||
trends30days: "1ヶ月間の増減"
|
trends30days: "1ヶ月間の増減"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "接続したプレイヤーの総数"
|
uniquePlayers: "接続したプレイヤーの総数"
|
||||||
uniquePlayers7days: "直近7日間のユニークプレイヤー数"
|
uniquePlayers7days: "直近7日間のユニークプレイヤー数"
|
||||||
unit:
|
unit:
|
||||||
percentage: "パーセンテージ"
|
percentage: "パーセンテージ"
|
||||||
playerCount: "プレイヤー数"
|
playerCount: "プレイヤー数"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "とてもログインしている"
|
veryActive: "とてもログインしている"
|
||||||
weekComparison: "直近1週間での比較"
|
weekComparison: "直近1週間での比較"
|
||||||
weekdays: "'月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'"
|
weekdays: "'月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU Usage"
|
cpuUsage: "CPU Usage"
|
||||||
currentPlayerbase: "현재 플레이어 베이스"
|
currentPlayerbase: "현재 플레이어 베이스"
|
||||||
currentUptime: "Current Uptime"
|
currentUptime: "Current Uptime"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Day by Day"
|
dayByDay: "Day by Day"
|
||||||
dayOfweek: "요일"
|
dayOfweek: "요일"
|
||||||
deadliestWeapon: "치명적인 PvP 무기"
|
deadliestWeapon: "치명적인 PvP 무기"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "정보"
|
information: "정보"
|
||||||
insights: "Insights"
|
insights: "Insights"
|
||||||
insights30days: "30일 동안의 인사이트"
|
insights30days: "30일 동안의 인사이트"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "불규칙한"
|
irregular: "불규칙한"
|
||||||
joinAddress: "Join Address"
|
joinAddress: "Join Address"
|
||||||
joinAddresses: "Join Addresses"
|
joinAddresses: "Join Addresses"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "몬스터한테 죽은 횟수"
|
mobDeaths: "몬스터한테 죽은 횟수"
|
||||||
mobKdr: "몬스터 KDR"
|
mobKdr: "몬스터 KDR"
|
||||||
mobKills: "몬스터 킬수"
|
mobKills: "몬스터 킬수"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "가장 활동적인 게임모드"
|
mostActiveGamemode: "가장 활동적인 게임모드"
|
||||||
mostPlayedWorld: "가장 많이 플레이 한 맵"
|
mostPlayedWorld: "가장 많이 플레이 한 맵"
|
||||||
name: "이름"
|
name: "이름"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Players Online (Now)"
|
playersOnlineNow: "Players Online (Now)"
|
||||||
playersOnlineOverview: "온라인 활동 개요"
|
playersOnlineOverview: "온라인 활동 개요"
|
||||||
playtime: "플레이타임"
|
playtime: "플레이타임"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "플러그인"
|
plugins: "플러그인"
|
||||||
pluginsOverview: "Plugins Overview"
|
pluginsOverview: "Plugins Overview"
|
||||||
punchcard: "펀치 카드"
|
punchcard: "펀치 카드"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "트렌드"
|
trend: "트렌드"
|
||||||
trends30days: "30일 동안의 트렌드"
|
trends30days: "30일 동안의 트렌드"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "기존 플레이어"
|
uniquePlayers: "기존 플레이어"
|
||||||
uniquePlayers7days: "Unique Players (7 days)"
|
uniquePlayers7days: "Unique Players (7 days)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Percentage"
|
percentage: "Percentage"
|
||||||
playerCount: "Player Count"
|
playerCount: "Player Count"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "매우 활성화된"
|
veryActive: "매우 활성화된"
|
||||||
weekComparison: "주 비교"
|
weekComparison: "주 비교"
|
||||||
weekdays: "'월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'"
|
weekdays: "'월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU Usage"
|
cpuUsage: "CPU Usage"
|
||||||
currentPlayerbase: "Huidige spelerbasis"
|
currentPlayerbase: "Huidige spelerbasis"
|
||||||
currentUptime: "Current Uptime"
|
currentUptime: "Current Uptime"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Dag voor dag"
|
dayByDay: "Dag voor dag"
|
||||||
dayOfweek: "Dag van de Week"
|
dayOfweek: "Dag van de Week"
|
||||||
deadliestWeapon: "Dodelijkste PvP-wapen"
|
deadliestWeapon: "Dodelijkste PvP-wapen"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "INFORMATIE"
|
information: "INFORMATIE"
|
||||||
insights: "Insights"
|
insights: "Insights"
|
||||||
insights30days: "Inzichten voor 30 dagen"
|
insights30days: "Inzichten voor 30 dagen"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Onregelmatig"
|
irregular: "Onregelmatig"
|
||||||
joinAddress: "Join Address"
|
joinAddress: "Join Address"
|
||||||
joinAddresses: "Inlog adressen"
|
joinAddresses: "Inlog adressen"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Mob veroorzaakt doden"
|
mobDeaths: "Mob veroorzaakt doden"
|
||||||
mobKdr: "Mob KDR"
|
mobKdr: "Mob KDR"
|
||||||
mobKills: "Mob Moorden"
|
mobKills: "Mob Moorden"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "Meest actieve spelmodus"
|
mostActiveGamemode: "Meest actieve spelmodus"
|
||||||
mostPlayedWorld: "Meest gespeelde wereld"
|
mostPlayedWorld: "Meest gespeelde wereld"
|
||||||
name: "Naam"
|
name: "Naam"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Players Online (Now)"
|
playersOnlineNow: "Players Online (Now)"
|
||||||
playersOnlineOverview: "Overzicht van online activiteiten"
|
playersOnlineOverview: "Overzicht van online activiteiten"
|
||||||
playtime: "Speeltijd"
|
playtime: "Speeltijd"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Plugins"
|
plugins: "Plugins"
|
||||||
pluginsOverview: "Plugins Overview"
|
pluginsOverview: "Plugins Overview"
|
||||||
punchcard: "Ponskaart"
|
punchcard: "Ponskaart"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Trend"
|
trend: "Trend"
|
||||||
trends30days: "Trends voor 30 dagen"
|
trends30days: "Trends voor 30 dagen"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Unieke spelers"
|
uniquePlayers: "Unieke spelers"
|
||||||
uniquePlayers7days: "Unique Players (7 days)"
|
uniquePlayers7days: "Unique Players (7 days)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Percentage"
|
percentage: "Percentage"
|
||||||
playerCount: "Player Count"
|
playerCount: "Player Count"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Heel Actief"
|
veryActive: "Heel Actief"
|
||||||
weekComparison: "Weekvergelijking"
|
weekComparison: "Weekvergelijking"
|
||||||
weekdays: "'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag', 'Zondag'"
|
weekdays: "'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag', 'Zondag'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU Usage"
|
cpuUsage: "CPU Usage"
|
||||||
currentPlayerbase: "Base de Jogadores Atual"
|
currentPlayerbase: "Base de Jogadores Atual"
|
||||||
currentUptime: "Current Uptime"
|
currentUptime: "Current Uptime"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Day by Day"
|
dayByDay: "Day by Day"
|
||||||
dayOfweek: "Day of the Week"
|
dayOfweek: "Day of the Week"
|
||||||
deadliestWeapon: "Deadliest PvP Weapon"
|
deadliestWeapon: "Deadliest PvP Weapon"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "INFORMATION"
|
information: "INFORMATION"
|
||||||
insights: "Insights"
|
insights: "Insights"
|
||||||
insights30days: "Insights for 30 days"
|
insights30days: "Insights for 30 days"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Irregular"
|
irregular: "Irregular"
|
||||||
joinAddress: "Join Address"
|
joinAddress: "Join Address"
|
||||||
joinAddresses: "Join Addresses"
|
joinAddresses: "Join Addresses"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Mortes causadas por Mobs"
|
mobDeaths: "Mortes causadas por Mobs"
|
||||||
mobKdr: "KDR por Mob"
|
mobKdr: "KDR por Mob"
|
||||||
mobKills: "Assassinato de Mobs"
|
mobKills: "Assassinato de Mobs"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "Most Active Gamemode"
|
mostActiveGamemode: "Most Active Gamemode"
|
||||||
mostPlayedWorld: "Most played World"
|
mostPlayedWorld: "Most played World"
|
||||||
name: "Nome"
|
name: "Nome"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Players Online (Now)"
|
playersOnlineNow: "Players Online (Now)"
|
||||||
playersOnlineOverview: "Online Activity Overview"
|
playersOnlineOverview: "Online Activity Overview"
|
||||||
playtime: "Tempo de Jogo"
|
playtime: "Tempo de Jogo"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Plugins"
|
plugins: "Plugins"
|
||||||
pluginsOverview: "Plugins Overview"
|
pluginsOverview: "Plugins Overview"
|
||||||
punchcard: "Punchcard"
|
punchcard: "Punchcard"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Trend"
|
trend: "Trend"
|
||||||
trends30days: "Trends for 30 days"
|
trends30days: "Trends for 30 days"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Jogadores Únicos"
|
uniquePlayers: "Jogadores Únicos"
|
||||||
uniquePlayers7days: "Unique Players (7 days)"
|
uniquePlayers7days: "Unique Players (7 days)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Percentage"
|
percentage: "Percentage"
|
||||||
playerCount: "Player Count"
|
playerCount: "Player Count"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Muito Ativo"
|
veryActive: "Muito Ativo"
|
||||||
weekComparison: "Week Comparison"
|
weekComparison: "Week Comparison"
|
||||||
weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"
|
weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "Использование ЦП"
|
cpuUsage: "Использование ЦП"
|
||||||
currentPlayerbase: "Текущая база игроков"
|
currentPlayerbase: "Текущая база игроков"
|
||||||
currentUptime: "Время безотказной работы"
|
currentUptime: "Время безотказной работы"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Статистика по дням"
|
dayByDay: "Статистика по дням"
|
||||||
dayOfweek: "День недели"
|
dayOfweek: "День недели"
|
||||||
deadliestWeapon: "Самое смертоносное оружие в PvP"
|
deadliestWeapon: "Самое смертоносное оружие в PvP"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "ИНФОРМАЦИЯ"
|
information: "ИНФОРМАЦИЯ"
|
||||||
insights: "Инсайты"
|
insights: "Инсайты"
|
||||||
insights30days: "Статистика за 30 дней"
|
insights30days: "Статистика за 30 дней"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Нерегулярный"
|
irregular: "Нерегулярный"
|
||||||
joinAddress: "Адрес входа"
|
joinAddress: "Адрес входа"
|
||||||
joinAddresses: "Адресы Входа"
|
joinAddresses: "Адресы Входа"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Смерть из-за мобов"
|
mobDeaths: "Смерть из-за мобов"
|
||||||
mobKdr: "Моб KDR"
|
mobKdr: "Моб KDR"
|
||||||
mobKills: "Убийства мобов"
|
mobKills: "Убийства мобов"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "Самый активный игровой режим"
|
mostActiveGamemode: "Самый активный игровой режим"
|
||||||
mostPlayedWorld: "Самый популярный мир"
|
mostPlayedWorld: "Самый популярный мир"
|
||||||
name: "Имя"
|
name: "Имя"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Players Online (Now)"
|
playersOnlineNow: "Players Online (Now)"
|
||||||
playersOnlineOverview: "Обзор сетевой активности"
|
playersOnlineOverview: "Обзор сетевой активности"
|
||||||
playtime: "Время игры"
|
playtime: "Время игры"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Плагины"
|
plugins: "Плагины"
|
||||||
pluginsOverview: "Обзор плагинов"
|
pluginsOverview: "Обзор плагинов"
|
||||||
punchcard: "Перфокарты"
|
punchcard: "Перфокарты"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Тенденция"
|
trend: "Тенденция"
|
||||||
trends30days: "тенденция за 30 дней"
|
trends30days: "тенденция за 30 дней"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Уникальные игроки"
|
uniquePlayers: "Уникальные игроки"
|
||||||
uniquePlayers7days: "Unique Players (7 days)"
|
uniquePlayers7days: "Unique Players (7 days)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Percentage"
|
percentage: "Percentage"
|
||||||
playerCount: "Player Count"
|
playerCount: "Player Count"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Очень активный"
|
veryActive: "Очень активный"
|
||||||
weekComparison: "Сравнение за неделю"
|
weekComparison: "Сравнение за неделю"
|
||||||
weekdays: "'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'"
|
weekdays: "'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU Usage"
|
cpuUsage: "CPU Usage"
|
||||||
currentPlayerbase: "Şuanki Oyuncu Tabanı"
|
currentPlayerbase: "Şuanki Oyuncu Tabanı"
|
||||||
currentUptime: "Current Uptime"
|
currentUptime: "Current Uptime"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Gün gün"
|
dayByDay: "Gün gün"
|
||||||
dayOfweek: "Day of the Week"
|
dayOfweek: "Day of the Week"
|
||||||
deadliestWeapon: "En Ölümcül PvP Silahı"
|
deadliestWeapon: "En Ölümcül PvP Silahı"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "DANIŞMA"
|
information: "DANIŞMA"
|
||||||
insights: "Insights"
|
insights: "Insights"
|
||||||
insights30days: "30 günlük bilgiler"
|
insights30days: "30 günlük bilgiler"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Düzensiz"
|
irregular: "Düzensiz"
|
||||||
joinAddress: "Join Address"
|
joinAddress: "Join Address"
|
||||||
joinAddresses: "Adreslere Katıl"
|
joinAddresses: "Adreslere Katıl"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Yaratık Yüzünden ölümler"
|
mobDeaths: "Yaratık Yüzünden ölümler"
|
||||||
mobKdr: "Mob İstatistiği"
|
mobKdr: "Mob İstatistiği"
|
||||||
mobKills: "Öldürülen Mob"
|
mobKills: "Öldürülen Mob"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "En Aktif Oyun Modu"
|
mostActiveGamemode: "En Aktif Oyun Modu"
|
||||||
mostPlayedWorld: "En çok oynanan Dünya"
|
mostPlayedWorld: "En çok oynanan Dünya"
|
||||||
name: "İsim"
|
name: "İsim"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Players Online (Now)"
|
playersOnlineNow: "Players Online (Now)"
|
||||||
playersOnlineOverview: "Çevrimiçi Etkinliğe Genel Bakış"
|
playersOnlineOverview: "Çevrimiçi Etkinliğe Genel Bakış"
|
||||||
playtime: "Oyun Süresi"
|
playtime: "Oyun Süresi"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Pluginler"
|
plugins: "Pluginler"
|
||||||
pluginsOverview: "Plugins Overview"
|
pluginsOverview: "Plugins Overview"
|
||||||
punchcard: "Punchcard"
|
punchcard: "Punchcard"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Trend"
|
trend: "Trend"
|
||||||
trends30days: "30 günlük trendler"
|
trends30days: "30 günlük trendler"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Sunucuya İlk Defa Girenler"
|
uniquePlayers: "Sunucuya İlk Defa Girenler"
|
||||||
uniquePlayers7days: "Unique Players (7 days)"
|
uniquePlayers7days: "Unique Players (7 days)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Percentage"
|
percentage: "Percentage"
|
||||||
playerCount: "Player Count"
|
playerCount: "Player Count"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Çok Aktif"
|
veryActive: "Çok Aktif"
|
||||||
weekComparison: "Hafta Karşılaştırması"
|
weekComparison: "Hafta Karşılaştırması"
|
||||||
weekdays: "'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'"
|
weekdays: "'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "Використання ЦП"
|
cpuUsage: "Використання ЦП"
|
||||||
currentPlayerbase: "Поточна база гравців"
|
currentPlayerbase: "Поточна база гравців"
|
||||||
currentUptime: "Час безвідмовної роботи"
|
currentUptime: "Час безвідмовної роботи"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "Статистика за днями"
|
dayByDay: "Статистика за днями"
|
||||||
dayOfweek: "День тижня"
|
dayOfweek: "День тижня"
|
||||||
deadliestWeapon: "Найсмертоносніша зброя в PvP"
|
deadliestWeapon: "Найсмертоносніша зброя в PvP"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "Інформація"
|
information: "Інформація"
|
||||||
insights: "Інсайти"
|
insights: "Інсайти"
|
||||||
insights30days: "Статистика за 30 днів"
|
insights30days: "Статистика за 30 днів"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "Нерегулярний"
|
irregular: "Нерегулярний"
|
||||||
joinAddress: "Адреса входу"
|
joinAddress: "Адреса входу"
|
||||||
joinAddresses: "Адреси входу"
|
joinAddresses: "Адреси входу"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "Смерть через мобів"
|
mobDeaths: "Смерть через мобів"
|
||||||
mobKdr: "Моб KDR"
|
mobKdr: "Моб KDR"
|
||||||
mobKills: "Вбивства мобів"
|
mobKills: "Вбивства мобів"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "Найактивніший ігровий режим"
|
mostActiveGamemode: "Найактивніший ігровий режим"
|
||||||
mostPlayedWorld: "Найпопулярніший світ"
|
mostPlayedWorld: "Найпопулярніший світ"
|
||||||
name: "Ім'я"
|
name: "Ім'я"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "Гравці онлайн (зараз)"
|
playersOnlineNow: "Гравці онлайн (зараз)"
|
||||||
playersOnlineOverview: "Огляд мережевої активності"
|
playersOnlineOverview: "Огляд мережевої активності"
|
||||||
playtime: "Час гри"
|
playtime: "Час гри"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "Плагіни"
|
plugins: "Плагіни"
|
||||||
pluginsOverview: "Огляд плагінів"
|
pluginsOverview: "Огляд плагінів"
|
||||||
punchcard: "Перфокарти"
|
punchcard: "Перфокарти"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "Тенденція"
|
trend: "Тенденція"
|
||||||
trends30days: "Тенденція за 30 днів"
|
trends30days: "Тенденція за 30 днів"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "Унікальні гравці"
|
uniquePlayers: "Унікальні гравці"
|
||||||
uniquePlayers7days: "Унікальні гравці (7 днів)"
|
uniquePlayers7days: "Унікальні гравці (7 днів)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Відсоток"
|
percentage: "Відсоток"
|
||||||
playerCount: "Кількість гравців"
|
playerCount: "Кількість гравців"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "Дуже активний"
|
veryActive: "Дуже активний"
|
||||||
weekComparison: "Порівняння за тиждень"
|
weekComparison: "Порівняння за тиждень"
|
||||||
weekdays: "'Понеділок', 'Вівторок', 'Середа', 'Четвер', 'П`ятниця', 'Субота', 'Неділя'"
|
weekdays: "'Понеділок', 'Вівторок', 'Середа', 'Четвер', 'П`ятниця', 'Субота', 'Неділя'"
|
||||||
|
|
|
@ -325,6 +325,7 @@ html:
|
||||||
cpuUsage: "CPU 使用率"
|
cpuUsage: "CPU 使用率"
|
||||||
currentPlayerbase: "目前玩家數量"
|
currentPlayerbase: "目前玩家數量"
|
||||||
currentUptime: "Current Uptime"
|
currentUptime: "Current Uptime"
|
||||||
|
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||||
dayByDay: "按天查看"
|
dayByDay: "按天查看"
|
||||||
dayOfweek: "星期"
|
dayOfweek: "星期"
|
||||||
deadliestWeapon: "最致命的 PvP 武器"
|
deadliestWeapon: "最致命的 PvP 武器"
|
||||||
|
@ -418,6 +419,7 @@ html:
|
||||||
information: "訊息"
|
information: "訊息"
|
||||||
insights: "Insights"
|
insights: "Insights"
|
||||||
insights30days: "30 天分析"
|
insights30days: "30 天分析"
|
||||||
|
installed: "Installed"
|
||||||
irregular: "偶爾上線"
|
irregular: "偶爾上線"
|
||||||
joinAddress: "Join Address"
|
joinAddress: "Join Address"
|
||||||
joinAddresses: "加入位址"
|
joinAddresses: "加入位址"
|
||||||
|
@ -469,6 +471,7 @@ html:
|
||||||
mobDeaths: "被生物擊殺數"
|
mobDeaths: "被生物擊殺數"
|
||||||
mobKdr: "生物 KDR"
|
mobKdr: "生物 KDR"
|
||||||
mobKills: "生物擊殺數"
|
mobKills: "生物擊殺數"
|
||||||
|
modified: "Modified"
|
||||||
mostActiveGamemode: "最常玩的遊戲模式"
|
mostActiveGamemode: "最常玩的遊戲模式"
|
||||||
mostPlayedWorld: "玩的最多的世界"
|
mostPlayedWorld: "玩的最多的世界"
|
||||||
name: "名稱"
|
name: "名稱"
|
||||||
|
@ -512,6 +515,8 @@ html:
|
||||||
playersOnlineNow: "線上玩家 (Now)"
|
playersOnlineNow: "線上玩家 (Now)"
|
||||||
playersOnlineOverview: "線上活動概覽"
|
playersOnlineOverview: "線上活動概覽"
|
||||||
playtime: "遊玩時間"
|
playtime: "遊玩時間"
|
||||||
|
pluginHistory: "Plugin History"
|
||||||
|
pluginVersionHistory: "Plugin Version History"
|
||||||
plugins: "插件"
|
plugins: "插件"
|
||||||
pluginsOverview: "插件概覽"
|
pluginsOverview: "插件概覽"
|
||||||
punchcard: "打卡"
|
punchcard: "打卡"
|
||||||
|
@ -599,12 +604,14 @@ html:
|
||||||
tps: "TPS"
|
tps: "TPS"
|
||||||
trend: "趨勢"
|
trend: "趨勢"
|
||||||
trends30days: "30 天趨勢"
|
trends30days: "30 天趨勢"
|
||||||
|
uninstalled: "Uninstalled"
|
||||||
uniquePlayers: "獨立玩家"
|
uniquePlayers: "獨立玩家"
|
||||||
uniquePlayers7days: "獨立玩家 (7 days)"
|
uniquePlayers7days: "獨立玩家 (7 days)"
|
||||||
unit:
|
unit:
|
||||||
percentage: "Percentage"
|
percentage: "Percentage"
|
||||||
playerCount: "Player Count"
|
playerCount: "Player Count"
|
||||||
users: "Manage Users"
|
users: "Manage Users"
|
||||||
|
version: "Version"
|
||||||
veryActive: "非常活躍"
|
veryActive: "非常活躍"
|
||||||
weekComparison: "每週對比"
|
weekComparison: "每週對比"
|
||||||
weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'"
|
weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'"
|
||||||
|
|
|
@ -155,7 +155,9 @@ class AccessControlTest {
|
||||||
Arguments.of("/v1/deleteGroup?group=admin&moveTo=no_access", WebPermission.MANAGE_GROUPS, 400, 403),
|
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/saveGroupPermissions?group=admin", WebPermission.MANAGE_GROUPS, 400, 403),
|
||||||
Arguments.of("/v1/preferences", WebPermission.ACCESS, 200, 200),
|
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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -146,6 +146,7 @@ class AccessControlVisibilityTest {
|
||||||
Arguments.arguments(WebPermission.PAGE_SERVER_PERFORMANCE_GRAPHS, "performance-graphs", "performance"),
|
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-as-numbers", "performance"),
|
||||||
Arguments.arguments(WebPermission.PAGE_SERVER_PERFORMANCE_OVERVIEW, "performance-insights", "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")
|
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_MAP, "geolocations", "geolocations"),
|
||||||
Arguments.arguments(WebPermission.PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY, "ping-per-country", "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_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")
|
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")
|
@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")
|
@MethodSource("pageLevelVisibleCases")
|
||||||
void pageNotVisible(WebPermission permission, String element, String page, Database database, ChromeDriver driver) throws Exception {
|
void pageNotVisible(WebPermission permission, String element, String page, Database database, ChromeDriver driver) throws Exception {
|
||||||
User user = registerUser(database);
|
User user = registerUser(database);
|
||||||
|
@ -272,7 +274,7 @@ class AccessControlVisibilityTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("Server element is not visible without permission")
|
@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")
|
@MethodSource("serverPageElementVisibleCases")
|
||||||
void serverPageElementNotVisible(WebPermission permission, String element, String section, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception {
|
void serverPageElementNotVisible(WebPermission permission, String element, String section, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception {
|
||||||
User user = registerUser(database, WebPermission.ACCESS_SERVER);
|
User user = registerUser(database, WebPermission.ACCESS_SERVER);
|
||||||
|
@ -310,7 +312,7 @@ class AccessControlVisibilityTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("Network element is not visible without permission")
|
@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")
|
@MethodSource("networkPageElementVisibleCases")
|
||||||
void networkPageElementNotVisible(WebPermission permission, String element, String section, Database database, ChromeDriver driver) throws Exception {
|
void networkPageElementNotVisible(WebPermission permission, String element, String section, Database database, ChromeDriver driver) throws Exception {
|
||||||
User user = registerUser(database, WebPermission.ACCESS_NETWORK);
|
User user = registerUser(database, WebPermission.ACCESS_NETWORK);
|
||||||
|
@ -343,7 +345,7 @@ class AccessControlVisibilityTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("Player element is not visible without permission")
|
@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")
|
@MethodSource("playerPageVisibleCases")
|
||||||
void playerPageElementNotVisible(WebPermission permission, String element, String section, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception {
|
void playerPageElementNotVisible(WebPermission permission, String element, String section, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception {
|
||||||
User user = registerUser(database, WebPermission.ACCESS_PLAYER);
|
User user = registerUser(database, WebPermission.ACCESS_PLAYER);
|
||||||
|
|
|
@ -91,6 +91,7 @@ class HttpAccessControlTest {
|
||||||
"/v1/saveGroupPermissions",
|
"/v1/saveGroupPermissions",
|
||||||
"/v1/deleteGroup",
|
"/v1/deleteGroup",
|
||||||
"/v1/storePreferences",
|
"/v1/storePreferences",
|
||||||
|
"/v1/pluginHistory?server=" + TestConstants.SERVER_UUID_STRING,
|
||||||
"/manage",
|
"/manage",
|
||||||
"/auth/register",
|
"/auth/register",
|
||||||
"/auth/login",
|
"/auth/login",
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Player Analytics (Plan).
|
||||||
|
*
|
||||||
|
* Plan is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Plan is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.djrapitops.plan.gathering.timed;
|
||||||
|
|
||||||
|
import com.djrapitops.plan.gathering.ServerSensor;
|
||||||
|
import com.djrapitops.plan.gathering.domain.PluginMetadata;
|
||||||
|
import com.djrapitops.plan.identification.ServerInfo;
|
||||||
|
import com.djrapitops.plan.storage.database.DBSystem;
|
||||||
|
import com.djrapitops.plan.storage.database.Database;
|
||||||
|
import com.djrapitops.plan.storage.database.transactions.Transaction;
|
||||||
|
import com.djrapitops.plan.storage.database.transactions.events.StorePluginVersionsTransaction;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import utilities.TestConstants;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests to ensure that plugin gathering works as intended.
|
||||||
|
*
|
||||||
|
* @author AuroraLS3
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class InstalledPluginGatheringTaskTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ServerSensor<?> serverSensor;
|
||||||
|
@Mock
|
||||||
|
ServerInfo serverInfo;
|
||||||
|
@Mock
|
||||||
|
DBSystem dbSystem;
|
||||||
|
@Mock
|
||||||
|
Database database;
|
||||||
|
|
||||||
|
InstalledPluginGatheringTask underTest;
|
||||||
|
|
||||||
|
Transaction capturedTransaction;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
when(database.executeTransaction(any())).then(invocation -> {
|
||||||
|
capturedTransaction = invocation.getArgument(0);
|
||||||
|
return CompletableFuture.allOf();
|
||||||
|
});
|
||||||
|
when(dbSystem.getDatabase()).thenReturn(database);
|
||||||
|
when(serverInfo.getServerUUID()).thenReturn(TestConstants.SERVER_UUID);
|
||||||
|
underTest = new InstalledPluginGatheringTask(serverSensor, serverInfo, dbSystem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void newPluginsAreIncluded() {
|
||||||
|
List<PluginMetadata> previouslyInstalledPlugins = List.of();
|
||||||
|
List<PluginMetadata> installedPlugins = List.of(
|
||||||
|
new PluginMetadata("Plan", "5.6 build 2121"),
|
||||||
|
new PluginMetadata("LittleChef", "1.0.2")
|
||||||
|
);
|
||||||
|
when(database.query(any())).thenReturn(previouslyInstalledPlugins);
|
||||||
|
when(serverSensor.getInstalledPlugins()).thenReturn(installedPlugins);
|
||||||
|
|
||||||
|
underTest.run();
|
||||||
|
|
||||||
|
List<PluginMetadata> changeList = ((StorePluginVersionsTransaction) capturedTransaction).getChangeList();
|
||||||
|
assertEquals(installedPlugins, changeList);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void onlyUpdatedPluginsAreIncluded() {
|
||||||
|
List<PluginMetadata> previouslyInstalledPlugins = List.of(
|
||||||
|
new PluginMetadata("Plan", "5.6 build 2121"),
|
||||||
|
new PluginMetadata("LittleChef", "1.0.2")
|
||||||
|
);
|
||||||
|
List<PluginMetadata> installedPlugins = List.of(
|
||||||
|
new PluginMetadata("Plan", "5.6 build 2121"),
|
||||||
|
new PluginMetadata("LittleChef", "1.0.3")
|
||||||
|
);
|
||||||
|
when(database.query(any())).thenReturn(previouslyInstalledPlugins);
|
||||||
|
when(serverSensor.getInstalledPlugins()).thenReturn(installedPlugins);
|
||||||
|
|
||||||
|
underTest.run();
|
||||||
|
|
||||||
|
List<PluginMetadata> expected = List.of(
|
||||||
|
new PluginMetadata("LittleChef", "1.0.3")
|
||||||
|
);
|
||||||
|
List<PluginMetadata> changeList = ((StorePluginVersionsTransaction) capturedTransaction).getChangeList();
|
||||||
|
assertEquals(expected, changeList);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void removedPluginsAreIncluded() {
|
||||||
|
List<PluginMetadata> previouslyInstalledPlugins = List.of(
|
||||||
|
new PluginMetadata("Plan", "5.6 build 2121"),
|
||||||
|
new PluginMetadata("LittleChef", "1.0.2")
|
||||||
|
);
|
||||||
|
List<PluginMetadata> installedPlugins = List.of(
|
||||||
|
new PluginMetadata("Plan", "5.6 build 2121")
|
||||||
|
);
|
||||||
|
when(database.query(any())).thenReturn(previouslyInstalledPlugins);
|
||||||
|
when(serverSensor.getInstalledPlugins()).thenReturn(installedPlugins);
|
||||||
|
|
||||||
|
underTest.run();
|
||||||
|
|
||||||
|
List<PluginMetadata> expected = List.of(
|
||||||
|
new PluginMetadata("LittleChef", null)
|
||||||
|
);
|
||||||
|
List<PluginMetadata> changeList = ((StorePluginVersionsTransaction) capturedTransaction).getChangeList();
|
||||||
|
assertEquals(expected, changeList);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.*;
|
||||||
import com.djrapitops.plan.storage.database.queries.analysis.PlayerRetentionQueriesTest;
|
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.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.ChangeUserUUIDTransactionTest;
|
||||||
import com.djrapitops.plan.storage.database.transactions.commands.CombineUserTransactionTest;
|
import com.djrapitops.plan.storage.database.transactions.commands.CombineUserTransactionTest;
|
||||||
import com.djrapitops.plan.storage.database.transactions.patches.AfterBadJoinAddressDataCorrectionPatchTest;
|
import com.djrapitops.plan.storage.database.transactions.patches.AfterBadJoinAddressDataCorrectionPatchTest;
|
||||||
|
@ -45,6 +46,7 @@ public interface DatabaseTestAggregate extends
|
||||||
ExtensionQueryResultTableDataQueryTest,
|
ExtensionQueryResultTableDataQueryTest,
|
||||||
BadJoinAddressDataCorrectionPatchTest,
|
BadJoinAddressDataCorrectionPatchTest,
|
||||||
AfterBadJoinAddressDataCorrectionPatchTest,
|
AfterBadJoinAddressDataCorrectionPatchTest,
|
||||||
PlayerRetentionQueriesTest {
|
PlayerRetentionQueriesTest,
|
||||||
|
PluginMetadataQueriesTest {
|
||||||
/* Collects all query tests together so its easier to implement database tests */
|
/* Collects all query tests together so its easier to implement database tests */
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Player Analytics (Plan).
|
||||||
|
*
|
||||||
|
* Plan is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Plan is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.djrapitops.plan.storage.database.queries.objects;
|
||||||
|
|
||||||
|
import com.djrapitops.plan.gathering.domain.PluginMetadata;
|
||||||
|
import com.djrapitops.plan.storage.database.DatabaseTestPreparer;
|
||||||
|
import com.djrapitops.plan.storage.database.transactions.events.StorePluginVersionsTransaction;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link PluginMetadataQueries}.
|
||||||
|
*
|
||||||
|
* @author AuroraLS3
|
||||||
|
*/
|
||||||
|
public interface PluginMetadataQueriesTest extends DatabaseTestPreparer {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Plugin Metadata is stored")
|
||||||
|
default void pluginMetadataIsStored() {
|
||||||
|
List<PluginMetadata> changeSet = List.of(
|
||||||
|
new PluginMetadata("Plan", "5.6 build 2121"),
|
||||||
|
new PluginMetadata("LittleChef", "1.0.2"),
|
||||||
|
new PluginMetadata("LittleFX", null)
|
||||||
|
);
|
||||||
|
db().executeTransaction(new StorePluginVersionsTransaction(System.currentTimeMillis(), serverUUID(), changeSet));
|
||||||
|
|
||||||
|
List<PluginMetadata> expected = List.of(
|
||||||
|
new PluginMetadata("Plan", "5.6 build 2121"),
|
||||||
|
new PluginMetadata("LittleChef", "1.0.2")
|
||||||
|
);
|
||||||
|
List<PluginMetadata> result = db().query(PluginMetadataQueries.getInstalledPlugins(serverUUID()));
|
||||||
|
assertEquals(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -17,6 +17,9 @@
|
||||||
package net.playeranalytics.plan.gathering;
|
package net.playeranalytics.plan.gathering;
|
||||||
|
|
||||||
import com.djrapitops.plan.gathering.ServerSensor;
|
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.entity.Entity;
|
||||||
import net.minecraft.server.dedicated.MinecraftDedicatedServer;
|
import net.minecraft.server.dedicated.MinecraftDedicatedServer;
|
||||||
import net.minecraft.server.world.ServerWorld;
|
import net.minecraft.server.world.ServerWorld;
|
||||||
|
@ -25,6 +28,7 @@ import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@ -89,4 +93,14 @@ public class FabricSensor implements ServerSensor<ServerWorld> {
|
||||||
public List<String> getOnlinePlayerNames() {
|
public List<String> getOnlinePlayerNames() {
|
||||||
return Arrays.asList(server.getPlayerNames());
|
return Arrays.asList(server.getPlayerNames());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PluginMetadata> getInstalledPlugins() {
|
||||||
|
return FabricLoaderImpl.INSTANCE.getMods().stream()
|
||||||
|
.map(ModContainer::getMetadata)
|
||||||
|
.map(metadata -> new PluginMetadata(
|
||||||
|
Optional.ofNullable(metadata.getName()).orElse(metadata.getId()),
|
||||||
|
metadata.getVersion().getFriendlyString()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
|
||||||
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
|
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
|
||||||
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
|
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
|
||||||
import com.djrapitops.plan.gathering.ShutdownHook;
|
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.ServerTPSCounter;
|
||||||
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
||||||
import com.djrapitops.plan.settings.upkeep.ConfigStoreTask;
|
import com.djrapitops.plan.settings.upkeep.ConfigStoreTask;
|
||||||
|
@ -98,4 +99,8 @@ public interface FabricTaskModule {
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,9 @@ package com.djrapitops.plan.gathering;
|
||||||
|
|
||||||
import cn.nukkit.Player;
|
import cn.nukkit.Player;
|
||||||
import cn.nukkit.level.Level;
|
import cn.nukkit.level.Level;
|
||||||
|
import cn.nukkit.plugin.Plugin;
|
||||||
import com.djrapitops.plan.PlanNukkit;
|
import com.djrapitops.plan.PlanNukkit;
|
||||||
|
import com.djrapitops.plan.gathering.domain.PluginMetadata;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
@ -74,4 +76,13 @@ public class NukkitSensor implements ServerSensor<Level> {
|
||||||
.map(Player::getName)
|
.map(Player::getName)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PluginMetadata> getInstalledPlugins() {
|
||||||
|
return plugin.getServer().getPluginManager()
|
||||||
|
.getPlugins().values().stream()
|
||||||
|
.map(Plugin::getDescription)
|
||||||
|
.map(description -> new PluginMetadata(description.getName(), description.getVersion()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
|
||||||
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
|
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
|
||||||
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
|
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
|
||||||
import com.djrapitops.plan.gathering.ShutdownHook;
|
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.NukkitPingCounter;
|
||||||
import com.djrapitops.plan.gathering.timed.ServerTPSCounter;
|
import com.djrapitops.plan.gathering.timed.ServerTPSCounter;
|
||||||
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
||||||
|
@ -98,4 +99,8 @@ public interface NukkitTaskModule {
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ const PlayerbaseOverview = React.lazy(() => import("./views/server/PlayerbaseOve
|
||||||
const ServerPlayers = React.lazy(() => import("./views/server/ServerPlayers"));
|
const ServerPlayers = React.lazy(() => import("./views/server/ServerPlayers"));
|
||||||
const ServerGeolocations = React.lazy(() => import("./views/server/ServerGeolocations"));
|
const ServerGeolocations = React.lazy(() => import("./views/server/ServerGeolocations"));
|
||||||
const ServerPerformance = React.lazy(() => import("./views/server/ServerPerformance"));
|
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 ServerPluginData = React.lazy(() => import("./views/server/ServerPluginData"));
|
||||||
const ServerWidePluginData = React.lazy(() => import("./views/server/ServerWidePluginData"));
|
const ServerWidePluginData = React.lazy(() => import("./views/server/ServerWidePluginData"));
|
||||||
const ServerJoinAddresses = React.lazy(() => import("./views/server/ServerJoinAddresses"));
|
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 NetworkGeolocations = React.lazy(() => import("./views/network/NetworkGeolocations"));
|
||||||
const NetworkPlayerbaseOverview = React.lazy(() => import("./views/network/NetworkPlayerbaseOverview"));
|
const NetworkPlayerbaseOverview = React.lazy(() => import("./views/network/NetworkPlayerbaseOverview"));
|
||||||
const NetworkPerformance = React.lazy(() => import("./views/network/NetworkPerformance"));
|
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 PlayersPage = React.lazy(() => import("./views/layout/PlayersPage"));
|
||||||
const AllPlayers = React.lazy(() => import("./views/players/AllPlayers"));
|
const AllPlayers = React.lazy(() => import("./views/players/AllPlayers"));
|
||||||
|
@ -163,6 +165,7 @@ function App() {
|
||||||
<Route path="players" element={<Lazy><ServerPlayers/></Lazy>}/>
|
<Route path="players" element={<Lazy><ServerPlayers/></Lazy>}/>
|
||||||
<Route path="geolocations" element={<Lazy><ServerGeolocations/></Lazy>}/>
|
<Route path="geolocations" element={<Lazy><ServerGeolocations/></Lazy>}/>
|
||||||
<Route path="performance" element={<Lazy><ServerPerformance/></Lazy>}/>
|
<Route path="performance" element={<Lazy><ServerPerformance/></Lazy>}/>
|
||||||
|
<Route path="plugin-history" element={<Lazy><ServerPluginHistory/></Lazy>}/>
|
||||||
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
|
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
|
||||||
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
|
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
|
||||||
<Route path="*" element={<ErrorView error={{
|
<Route path="*" element={<ErrorView error={{
|
||||||
|
@ -183,6 +186,7 @@ function App() {
|
||||||
<Route path="join-addresses" element={<Lazy><NetworkJoinAddresses/></Lazy>}/>
|
<Route path="join-addresses" element={<Lazy><NetworkJoinAddresses/></Lazy>}/>
|
||||||
<Route path="players" element={<Lazy><AllPlayers/></Lazy>}/>
|
<Route path="players" element={<Lazy><AllPlayers/></Lazy>}/>
|
||||||
<Route path="geolocations" element={<Lazy><NetworkGeolocations/></Lazy>}/>
|
<Route path="geolocations" element={<Lazy><NetworkGeolocations/></Lazy>}/>
|
||||||
|
<Route path="plugin-history" element={<Lazy><NetworkPluginHistory/></Lazy>}/>
|
||||||
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
|
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
|
||||||
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
|
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
|
||||||
<Route path="*" element={<ErrorView error={{
|
<Route path="*" element={<ErrorView error={{
|
||||||
|
|
|
@ -2,11 +2,12 @@ import React, {useEffect, useState} from "react";
|
||||||
import {useLocation, useNavigate} from "react-router-dom";
|
import {useLocation, useNavigate} from "react-router-dom";
|
||||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
const TabButton = ({id, name, href, icon, color, active}) => {
|
const TabButton = ({id, name, href, icon, color, active, disabled}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<li className="nav-item" id={id}>
|
<li className={"nav-item" + (disabled ? ' disabled' : '')} id={id}>
|
||||||
<button className={"nav-link col-black" + (active ? ' active' : '')} aria-selected={active} role="tab"
|
<button disabled={disabled} className={"nav-link col-black" + (active ? ' active' : '')}
|
||||||
|
aria-selected={active} role="tab"
|
||||||
onClick={() => navigate('#' + href, {replace: true})}>
|
onClick={() => navigate('#' + href, {replace: true})}>
|
||||||
<Fa icon={icon} className={'col-' + color}/> {name}
|
<Fa icon={icon} className={'col-' + color}/> {name}
|
||||||
</button>
|
</button>
|
||||||
|
@ -26,6 +27,7 @@ const TabButtons = ({tabs, selectedTab}) => {
|
||||||
icon={tab.icon}
|
icon={tab.icon}
|
||||||
color={tab.color}
|
color={tab.color}
|
||||||
active={tab.href === selectedTab}
|
active={tab.href === selectedTab}
|
||||||
|
disabled={tab.disabled}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React from 'react';
|
||||||
|
import CardHeader from "../CardHeader";
|
||||||
|
import {faCube, faCubes, faSignal} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import {Card} from "react-bootstrap";
|
||||||
|
import {ErrorViewCard} from "../../../views/ErrorView";
|
||||||
|
import FormattedDate from "../../text/FormattedDate";
|
||||||
|
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faCalendar} from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
import {CardLoader} from "../../navigation/Loader";
|
||||||
|
import DataTablesTable from "../../table/DataTablesTable";
|
||||||
|
|
||||||
|
const PluginCurrentCard = ({data, loadingError}) => {
|
||||||
|
const {t} = useTranslation();
|
||||||
|
|
||||||
|
if (loadingError) return <ErrorViewCard error={loadingError}/>;
|
||||||
|
if (!data) return <CardLoader/>;
|
||||||
|
|
||||||
|
const history = [];
|
||||||
|
|
||||||
|
for (const entry of data.history) {
|
||||||
|
if (history.find(e => e.name === entry.name)) continue;
|
||||||
|
history.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = {
|
||||||
|
columns: [{
|
||||||
|
title: <><Fa icon={faCube}/> {t('html.label.name')}</>,
|
||||||
|
data: "name"
|
||||||
|
}, {
|
||||||
|
title: <><Fa icon={faSignal}/> {t('html.label.version')}</>,
|
||||||
|
data: "version"
|
||||||
|
}, {
|
||||||
|
title: <><Fa icon={faCalendar}/> {t('html.label.modified')}</>,
|
||||||
|
data: {_: "modified", display: "modifiedDisplay"}
|
||||||
|
}],
|
||||||
|
data: history.length ? history.filter(entry => entry.version)
|
||||||
|
.map(entry => {
|
||||||
|
return {
|
||||||
|
name: entry.name,
|
||||||
|
version: t(entry.version),
|
||||||
|
modified: entry.modified,
|
||||||
|
modifiedDisplay: <FormattedDate date={entry.modified}/>
|
||||||
|
}
|
||||||
|
}) : [{name: t('generic.noData'), version: '', 'modified': 0, modifiedDisplay: ''}]
|
||||||
|
};
|
||||||
|
const options = {
|
||||||
|
responsive: true,
|
||||||
|
deferRender: true,
|
||||||
|
columns: table.columns,
|
||||||
|
data: table.data,
|
||||||
|
pagingType: "numbers",
|
||||||
|
order: [[0, "desc"]]
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowKeyFunction = (row, column) => {
|
||||||
|
return row.name + "-" + row.version + '-' + row.modified + '-' + JSON.stringify(column?.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader icon={faCubes} label={'html.label.currentlyInstalledPlugins'} color={"indigo"}/>
|
||||||
|
<DataTablesTable id={"plugin-current"} options={options} rowKeyFunction={rowKeyFunction}/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginCurrentCard
|
|
@ -0,0 +1,62 @@
|
||||||
|
import React from 'react';
|
||||||
|
import CardHeader from "../CardHeader";
|
||||||
|
import {faCodeCompare, faCube, faSignal} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import {Card} from "react-bootstrap";
|
||||||
|
import {ErrorViewCard} from "../../../views/ErrorView";
|
||||||
|
import FormattedDate from "../../text/FormattedDate";
|
||||||
|
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faCalendar} from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
import {CardLoader} from "../../navigation/Loader";
|
||||||
|
import DataTablesTable from "../../table/DataTablesTable";
|
||||||
|
|
||||||
|
const PluginHistoryCard = ({data, loadingError}) => {
|
||||||
|
const {t} = useTranslation();
|
||||||
|
|
||||||
|
if (loadingError) return <ErrorViewCard error={loadingError}/>;
|
||||||
|
if (!data) return <CardLoader/>;
|
||||||
|
|
||||||
|
const history = data.history;
|
||||||
|
|
||||||
|
const table = {
|
||||||
|
columns: [{
|
||||||
|
title: <><Fa icon={faCube}/> {t('html.label.name')}</>,
|
||||||
|
data: "name"
|
||||||
|
}, {
|
||||||
|
title: <><Fa icon={faSignal}/> {t('html.label.version')}</>,
|
||||||
|
data: "version"
|
||||||
|
}, {
|
||||||
|
title: <><Fa icon={faCalendar}/> {t('html.label.modified')}</>,
|
||||||
|
data: {_: "modified", display: "modifiedDisplay"}
|
||||||
|
}],
|
||||||
|
data: history.length ? history.map(entry => {
|
||||||
|
return {
|
||||||
|
name: entry.name,
|
||||||
|
version: t(entry.version || 'html.label.uninstalled'),
|
||||||
|
modified: entry.modified,
|
||||||
|
modifiedDisplay: <FormattedDate date={entry.modified}/>
|
||||||
|
}
|
||||||
|
}) : [{name: t('generic.noData'), version: '', 'modified': 0, modifiedDisplay: ''}]
|
||||||
|
};
|
||||||
|
const options = {
|
||||||
|
responsive: true,
|
||||||
|
deferRender: true,
|
||||||
|
columns: table.columns,
|
||||||
|
data: table.data,
|
||||||
|
pagingType: "numbers",
|
||||||
|
order: [[2, "desc"]]
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowKeyFunction = (row, column) => {
|
||||||
|
return row.name + "-" + row.version + '-' + row.modified + '-' + JSON.stringify(column?.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader icon={faCodeCompare} label={'html.label.pluginVersionHistory'} color={"indigo"}/>
|
||||||
|
<DataTablesTable id={"plugin-history"} options={options} rowKeyFunction={rowKeyFunction}/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginHistoryCard
|
|
@ -153,13 +153,16 @@ const PerformanceGraphsCard = ({data}) => {
|
||||||
setPerformanceSeries(series);
|
setPerformanceSeries(series);
|
||||||
}, [data, setPerformanceSeries])
|
}, [data, setPerformanceSeries])
|
||||||
|
|
||||||
|
const dataIncludesGameServers = useMemo(() => data.servers && Boolean(data.servers.filter(server => !server.proxy).length), [data]);
|
||||||
|
|
||||||
const tabs = useMemo(() => [
|
const tabs = useMemo(() => [
|
||||||
{
|
{
|
||||||
name: t('html.label.playersOnline'), icon: faUser, color: 'light-blue', href: 'players-online',
|
name: t('html.label.playersOnline'), icon: faUser, color: 'light-blue', href: 'players-online',
|
||||||
element: <Tab data={performanceSeries.players} yAxis={yAxisConfigurations.PLAYERS_ONLINE}/>
|
element: <Tab data={performanceSeries.players} yAxis={yAxisConfigurations.PLAYERS_ONLINE}/>
|
||||||
}, {
|
}, {
|
||||||
name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps',
|
name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps',
|
||||||
element: <Tab data={performanceSeries.tps} yAxis={yAxisConfigurations.TPS}/>
|
element: <Tab data={performanceSeries.tps} yAxis={yAxisConfigurations.TPS}/>,
|
||||||
|
disabled: !dataIncludesGameServers
|
||||||
}, {
|
}, {
|
||||||
name: t('html.label.cpu'), icon: faTachometerAlt, color: 'amber', href: 'cpu',
|
name: t('html.label.cpu'), icon: faTachometerAlt, color: 'amber', href: 'cpu',
|
||||||
element: <Tab data={performanceSeries.cpu} yAxis={yAxisConfigurations.CPU}/>
|
element: <Tab data={performanceSeries.cpu} yAxis={yAxisConfigurations.CPU}/>
|
||||||
|
@ -168,10 +171,12 @@ const PerformanceGraphsCard = ({data}) => {
|
||||||
element: <Tab data={performanceSeries.ram} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
|
element: <Tab data={performanceSeries.ram} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
|
||||||
}, {
|
}, {
|
||||||
name: t('html.label.entities'), icon: faDragon, color: 'purple', href: 'entities',
|
name: t('html.label.entities'), icon: faDragon, color: 'purple', href: 'entities',
|
||||||
element: <Tab data={performanceSeries.entities} yAxis={yAxisConfigurations.ENTITIES}/>
|
element: <Tab data={performanceSeries.entities} yAxis={yAxisConfigurations.ENTITIES}/>,
|
||||||
|
disabled: !dataIncludesGameServers
|
||||||
}, {
|
}, {
|
||||||
name: t('html.label.loadedChunks'), icon: faMap, color: 'blue-grey', href: 'chunks',
|
name: t('html.label.loadedChunks'), icon: faMap, color: 'blue-grey', href: 'chunks',
|
||||||
element: <Tab data={performanceSeries.chunks} yAxis={yAxisConfigurations.CHUNKS}/>
|
element: <Tab data={performanceSeries.chunks} yAxis={yAxisConfigurations.CHUNKS}/>,
|
||||||
|
disabled: !dataIncludesGameServers
|
||||||
}, {
|
}, {
|
||||||
name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk',
|
name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk',
|
||||||
element: <Tab data={performanceSeries.disk} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
|
element: <Tab data={performanceSeries.disk} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
|
||||||
|
@ -180,7 +185,7 @@ const PerformanceGraphsCard = ({data}) => {
|
||||||
element: networkMetadata ? <PingTab identifier={networkMetadata.currentServer.serverUUID}/> :
|
element: networkMetadata ? <PingTab identifier={networkMetadata.currentServer.serverUUID}/> :
|
||||||
<ChartLoader/>
|
<ChartLoader/>
|
||||||
},
|
},
|
||||||
], [performanceSeries, networkMetadata, t]);
|
], [performanceSeries, networkMetadata, t, dataIncludesGameServers]);
|
||||||
|
|
||||||
if (!data || !Object.values(data).length) return <CardLoader/>
|
if (!data || !Object.values(data).length) return <CardLoader/>
|
||||||
if (data.errors.length) {
|
if (data.errors.length) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {useParams} from "react-router-dom";
|
import {useParams} from "react-router-dom";
|
||||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||||
import {fetchOptimizedPerformance, fetchPingGraph} from "../../../../service/serverService";
|
import {fetchOptimizedPerformance, fetchPingGraph, fetchPluginHistory} from "../../../../service/serverService";
|
||||||
import {ErrorViewBody} from "../../../../views/ErrorView";
|
import {ErrorViewBody} from "../../../../views/ErrorView";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {Card} from "react-bootstrap";
|
import {Card} from "react-bootstrap";
|
||||||
|
@ -15,40 +15,46 @@ import WorldPerformanceGraph from "../../../graphs/performance/WorldPerformanceG
|
||||||
import DiskPerformanceGraph from "../../../graphs/performance/DiskPerformanceGraph";
|
import DiskPerformanceGraph from "../../../graphs/performance/DiskPerformanceGraph";
|
||||||
import PingGraph from "../../../graphs/performance/PingGraph";
|
import PingGraph from "../../../graphs/performance/PingGraph";
|
||||||
import {mapPerformanceDataToSeries} from "../../../../util/graphs";
|
import {mapPerformanceDataToSeries} from "../../../../util/graphs";
|
||||||
|
import {useAuth} from "../../../../hooks/authenticationHook";
|
||||||
|
|
||||||
const AllGraphTab = ({data, dataSeries, loadingError}) => {
|
const AllGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => {
|
||||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||||
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
|
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
|
||||||
|
|
||||||
return <AllPerformanceGraph id="server-performance-all-chart" data={data} dataSeries={dataSeries}/>
|
return <AllPerformanceGraph id="server-performance-all-chart" data={data} dataSeries={dataSeries}
|
||||||
|
pluginHistorySeries={pluginHistorySeries}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
const TpsGraphTab = ({data, dataSeries, loadingError}) => {
|
const TpsGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => {
|
||||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||||
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
|
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
|
||||||
|
|
||||||
return <TpsPerformanceGraph id="server-performance-tps-chart" data={data} dataSeries={dataSeries}/>
|
return <TpsPerformanceGraph id="server-performance-tps-chart" data={data} dataSeries={dataSeries}
|
||||||
|
pluginHistorySeries={pluginHistorySeries}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
const CpuRamGraphTab = ({data, dataSeries, loadingError}) => {
|
const CpuRamGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => {
|
||||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||||
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
|
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
|
||||||
|
|
||||||
return <CpuRamPerformanceGraph id="server-performance-cpuram-chart" data={data} dataSeries={dataSeries}/>
|
return <CpuRamPerformanceGraph id="server-performance-cpuram-chart" data={data} dataSeries={dataSeries}
|
||||||
|
pluginHistorySeries={pluginHistorySeries}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
const WorldGraphTab = ({data, dataSeries, loadingError}) => {
|
const WorldGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => {
|
||||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||||
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
|
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
|
||||||
|
|
||||||
return <WorldPerformanceGraph id="server-performance-world-chart" data={data} dataSeries={dataSeries}/>
|
return <WorldPerformanceGraph id="server-performance-world-chart" data={data} dataSeries={dataSeries}
|
||||||
|
pluginHistorySeries={pluginHistorySeries}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiskGraphTab = ({data, dataSeries, loadingError}) => {
|
const DiskGraphTab = ({data, dataSeries, pluginHistorySeries, loadingError}) => {
|
||||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||||
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
|
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
|
||||||
|
|
||||||
return <DiskPerformanceGraph id="server-performance-disk-chart" data={data} dataSeries={dataSeries}/>
|
return <DiskPerformanceGraph id="server-performance-disk-chart" data={data} dataSeries={dataSeries}
|
||||||
|
pluginHistorySeries={pluginHistorySeries}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
const PingGraphTab = ({identifier}) => {
|
const PingGraphTab = ({identifier}) => {
|
||||||
|
@ -61,37 +67,79 @@ const PingGraphTab = ({identifier}) => {
|
||||||
|
|
||||||
const PerformanceGraphsCard = () => {
|
const PerformanceGraphsCard = () => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
const {authRequired, hasPermission} = useAuth();
|
||||||
|
|
||||||
const {identifier} = useParams();
|
const {identifier} = useParams();
|
||||||
const {data, loadingError} = useDataRequest(fetchOptimizedPerformance, [identifier]);
|
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(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
mapPerformanceDataToSeries(data.values).then(parsed => setParsedData(parsed))
|
mapPerformanceDataToSeries(data.values).then(parsed => setParsedData(parsed))
|
||||||
}
|
}
|
||||||
}, [data, setParsedData]);
|
}, [data, setParsedData]);
|
||||||
|
useEffect(() => {
|
||||||
|
// https://stackoverflow.com/a/34890276/20825073
|
||||||
|
const groupBy = function (xs, key) {
|
||||||
|
return xs.reduce(function (rv, x) {
|
||||||
|
(rv[x[key]] = rv[x[key]] || []).push(x);
|
||||||
|
return rv;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pluginHistory) {
|
||||||
|
const grouped = groupBy(pluginHistory.history.reverse(), 'modified');
|
||||||
|
setPluginHistorySeries({
|
||||||
|
type: 'flags',
|
||||||
|
accessibility: {
|
||||||
|
exposeAsGroupOnly: true,
|
||||||
|
description: t('html.label.pluginVersionHistory')
|
||||||
|
},
|
||||||
|
tooltip: {headerFormat: ''},
|
||||||
|
data: Object.entries(grouped).map(entry => {
|
||||||
|
const installedLines = entry[1].filter(p => p.version).map(plugin => plugin.name + ': ' + plugin.version).join(', <br>');
|
||||||
|
const uninstalledLines = entry[1].filter(p => !p.version).map(plugin => plugin.name).join(', <br>');
|
||||||
|
return {
|
||||||
|
x: entry[0],
|
||||||
|
title: entry[1].length,
|
||||||
|
text: (installedLines.length ? '<b>' + t('html.label.installed') + '</b><br>' + installedLines : '') +
|
||||||
|
(uninstalledLines.length ? '<b>' + t('html.label.uninstalled') + '</b><br>' + uninstalledLines : '')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [pluginHistory, setPluginHistorySeries, t]);
|
||||||
|
|
||||||
return <Card id={"performance-graphs"}>
|
return <Card id={"performance-graphs"}>
|
||||||
<CardTabs tabs={[
|
<CardTabs tabs={[
|
||||||
{
|
{
|
||||||
name: t('html.label.all'), icon: faGears, color: 'blue-grey', href: 'all',
|
name: t('html.label.all'), icon: faGears, color: 'blue-grey', href: 'all',
|
||||||
element: <AllGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
|
element: <AllGraphTab data={data} dataSeries={parsedData} pluginHistorySeries={pluginHistorySeries}
|
||||||
|
loadingError={loadingError || pluginHistoryLoadingError}/>
|
||||||
}, {
|
}, {
|
||||||
name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps',
|
name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps',
|
||||||
element: <TpsGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
|
element: <TpsGraphTab data={data} dataSeries={parsedData} pluginHistorySeries={pluginHistorySeries}
|
||||||
|
loadingError={loadingError || pluginHistoryLoadingError}/>
|
||||||
}, {
|
}, {
|
||||||
name: t('html.label.cpuRam'), icon: faMicrochip, color: 'light-green', href: 'cpu-ram',
|
name: t('html.label.cpuRam'), icon: faMicrochip, color: 'light-green', href: 'cpu-ram',
|
||||||
element: <CpuRamGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
|
element: <CpuRamGraphTab data={data} dataSeries={parsedData} pluginHistorySeries={pluginHistorySeries}
|
||||||
|
loadingError={loadingError || pluginHistoryLoadingError}/>
|
||||||
}, {
|
}, {
|
||||||
name: t('html.label.world'), icon: faMap, color: 'purple', href: 'world-load',
|
name: t('html.label.world'), icon: faMap, color: 'purple', href: 'world-load',
|
||||||
element: <WorldGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
|
element: <WorldGraphTab data={data} dataSeries={parsedData} pluginHistorySeries={pluginHistorySeries}
|
||||||
|
loadingError={loadingError || pluginHistoryLoadingError}/>
|
||||||
}, {
|
}, {
|
||||||
name: t('html.label.ping'), icon: faSignal, color: 'amber', href: 'ping',
|
name: t('html.label.ping'), icon: faSignal, color: 'amber', href: 'ping',
|
||||||
element: <PingGraphTab identifier={identifier}/>
|
element: <PingGraphTab identifier={identifier}/>
|
||||||
}, {
|
}, {
|
||||||
name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk',
|
name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk',
|
||||||
element: <DiskGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
|
element: <DiskGraphTab data={data} dataSeries={parsedData} pluginHistorySeries={pluginHistorySeries}
|
||||||
|
loadingError={loadingError || pluginHistoryLoadingError}/>
|
||||||
},
|
},
|
||||||
]}/>
|
]}/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PerformanceAsNumbersTable from "../../../table/PerformanceAsNumbersTable";
|
import PerformanceAsNumbersTable from "../../../table/PerformanceAsNumbersTable";
|
||||||
import CardHeader from "../../CardHeader";
|
import CardHeader from "../../CardHeader";
|
||||||
import {faBookOpen} from "@fortawesome/free-solid-svg-icons";
|
import {faBookOpen, faInfoCircle} from "@fortawesome/free-solid-svg-icons";
|
||||||
import {Card} from "react-bootstrap";
|
import {Alert, Card} from "react-bootstrap";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
const PerformanceAsNumbersCard = ({data, servers}) => {
|
const PerformanceAsNumbersCard = ({data, servers}) => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
@ -15,11 +16,15 @@ const PerformanceAsNumbersCard = ({data, servers}) => {
|
||||||
: (noData7d ? <p className={"alert alert-warning mb-0"}>{t('html.description.noData7d')}</p>
|
: (noData7d ? <p className={"alert alert-warning mb-0"}>{t('html.description.noData7d')}</p>
|
||||||
: (noData24h ? <p className={"alert alert-warning mb-0"}>{t('html.description.noData24h')}</p>
|
: (noData24h ? <p className={"alert alert-warning mb-0"}>{t('html.description.noData24h')}</p>
|
||||||
: ''));
|
: ''));
|
||||||
|
const dataIncludesGameServers = !servers || Boolean(servers.filter(server => !server.proxy).length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card id={"performance-as-numbers"}>
|
<Card id={"performance-as-numbers"}>
|
||||||
<CardHeader icon={faBookOpen} color="blue-grey" label={"html.label.performanceAsNumbers"}/>
|
<CardHeader icon={faBookOpen} color="blue-grey" label={"html.label.performanceAsNumbers"}/>
|
||||||
{noDataAlert}
|
{noDataAlert}
|
||||||
|
{!dataIncludesGameServers && <Alert className='alert-warning mb-0'>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle}/> {t('html.description.performanceNoGameServers')}
|
||||||
|
</Alert>}
|
||||||
<PerformanceAsNumbersTable data={data} servers={servers}/>
|
<PerformanceAsNumbersTable data={data} servers={servers}/>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|
|
@ -62,7 +62,7 @@ const yAxis = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const AllPerformanceGraph = ({id, data, dataSeries}) => {
|
const AllPerformanceGraph = ({id, data, dataSeries, pluginHistorySeries}) => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const {graphTheming, nightModeEnabled} = useTheme();
|
const {graphTheming, nightModeEnabled} = useTheme();
|
||||||
const {timeZoneOffsetMinutes} = useMetadata();
|
const {timeZoneOffsetMinutes} = useMetadata();
|
||||||
|
@ -178,9 +178,9 @@ const AllPerformanceGraph = ({id, data, dataSeries}) => {
|
||||||
time: {
|
time: {
|
||||||
timezoneOffset: timeZoneOffsetMinutes
|
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 (
|
return (
|
||||||
<div className="chart-area" style={{height: "450px"}} id={id}>
|
<div className="chart-area" style={{height: "450px"}} id={id}>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {withReducedSaturation} from "../../../util/colors";
|
||||||
import Accessibility from "highcharts/modules/accessibility";
|
import Accessibility from "highcharts/modules/accessibility";
|
||||||
import {useMetadata} from "../../../hooks/metadataHook";
|
import {useMetadata} from "../../../hooks/metadataHook";
|
||||||
|
|
||||||
const CpuRamPerformanceGraph = ({id, data, dataSeries}) => {
|
const CpuRamPerformanceGraph = ({id, data, dataSeries, pluginHistorySeries}) => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const {graphTheming, nightModeEnabled} = useTheme();
|
const {graphTheming, nightModeEnabled} = useTheme();
|
||||||
const {timeZoneOffsetMinutes} = useMetadata();
|
const {timeZoneOffsetMinutes} = useMetadata();
|
||||||
|
@ -90,9 +90,9 @@ const CpuRamPerformanceGraph = ({id, data, dataSeries}) => {
|
||||||
time: {
|
time: {
|
||||||
timezoneOffset: timeZoneOffsetMinutes
|
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 (
|
return (
|
||||||
<div className="chart-area" style={{height: "450px"}} id={id}>
|
<div className="chart-area" style={{height: "450px"}} id={id}>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {withReducedSaturation} from "../../../util/colors";
|
||||||
import Accessibility from "highcharts/modules/accessibility";
|
import Accessibility from "highcharts/modules/accessibility";
|
||||||
import {useMetadata} from "../../../hooks/metadataHook";
|
import {useMetadata} from "../../../hooks/metadataHook";
|
||||||
|
|
||||||
const DiskPerformanceGraph = ({id, data, dataSeries}) => {
|
const DiskPerformanceGraph = ({id, data, dataSeries, pluginHistorySeries}) => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const {graphTheming, nightModeEnabled} = useTheme();
|
const {graphTheming, nightModeEnabled} = useTheme();
|
||||||
const {timeZoneOffsetMinutes} = useMetadata();
|
const {timeZoneOffsetMinutes} = useMetadata();
|
||||||
|
@ -71,9 +71,9 @@ const DiskPerformanceGraph = ({id, data, dataSeries}) => {
|
||||||
time: {
|
time: {
|
||||||
timezoneOffset: timeZoneOffsetMinutes
|
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 (
|
return (
|
||||||
<div className="chart-area" style={{height: "450px"}} id={id}>
|
<div className="chart-area" style={{height: "450px"}} id={id}>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {withReducedSaturation} from "../../../util/colors";
|
||||||
import Accessibility from "highcharts/modules/accessibility";
|
import Accessibility from "highcharts/modules/accessibility";
|
||||||
import {useMetadata} from "../../../hooks/metadataHook";
|
import {useMetadata} from "../../../hooks/metadataHook";
|
||||||
|
|
||||||
const TpsPerformanceGraph = ({id, data, dataSeries}) => {
|
const TpsPerformanceGraph = ({id, data, dataSeries, pluginHistorySeries}) => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const {graphTheming, nightModeEnabled} = useTheme();
|
const {graphTheming, nightModeEnabled} = useTheme();
|
||||||
const {timeZoneOffsetMinutes} = useMetadata();
|
const {timeZoneOffsetMinutes} = useMetadata();
|
||||||
|
@ -87,9 +87,9 @@ const TpsPerformanceGraph = ({id, data, dataSeries}) => {
|
||||||
time: {
|
time: {
|
||||||
timezoneOffset: timeZoneOffsetMinutes
|
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 (
|
return (
|
||||||
<div className="chart-area" style={{height: "450px"}} id={id}>
|
<div className="chart-area" style={{height: "450px"}} id={id}>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {withReducedSaturation} from "../../../util/colors";
|
||||||
import Accessibility from "highcharts/modules/accessibility";
|
import Accessibility from "highcharts/modules/accessibility";
|
||||||
import {useMetadata} from "../../../hooks/metadataHook";
|
import {useMetadata} from "../../../hooks/metadataHook";
|
||||||
|
|
||||||
const WorldPerformanceGraph = ({id, data, dataSeries}) => {
|
const WorldPerformanceGraph = ({id, data, dataSeries, pluginHistorySeries}) => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const {graphTheming, nightModeEnabled} = useTheme();
|
const {graphTheming, nightModeEnabled} = useTheme();
|
||||||
const {timeZoneOffsetMinutes} = useMetadata();
|
const {timeZoneOffsetMinutes} = useMetadata();
|
||||||
|
@ -89,9 +89,9 @@ const WorldPerformanceGraph = ({id, data, dataSeries}) => {
|
||||||
time: {
|
time: {
|
||||||
timezoneOffset: timeZoneOffsetMinutes
|
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 (
|
return (
|
||||||
<div className="chart-area" style={{height: "450px"}} id={id}>
|
<div className="chart-area" style={{height: "450px"}} id={id}>
|
||||||
|
|
|
@ -332,4 +332,10 @@ const fetchNetworkPlayerJoinAddresses = async (timestamp) => {
|
||||||
let url = `/v1/joinAddresses`;
|
let url = `/v1/joinAddresses`;
|
||||||
if (staticSite) url = `/data/joinAddresses.json`;
|
if (staticSite) url = `/data/joinAddresses.json`;
|
||||||
return doGetRequest(url, timestamp);
|
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);
|
||||||
}
|
}
|
|
@ -1495,4 +1495,8 @@ ul.filters {
|
||||||
|
|
||||||
.group-help i {
|
.group-help i {
|
||||||
color: var(--color-plan)
|
color: var(--color-plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.disabled {
|
||||||
|
opacity: 30%;
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ import {Outlet} from "react-router-dom";
|
||||||
import {useNavigation} from "../../hooks/navigationHook";
|
import {useNavigation} from "../../hooks/navigationHook";
|
||||||
import {
|
import {
|
||||||
faChartLine,
|
faChartLine,
|
||||||
|
faCodeCompare,
|
||||||
faCogs,
|
faCogs,
|
||||||
faCubes,
|
faCubes,
|
||||||
faGlobe,
|
faGlobe,
|
||||||
|
@ -32,13 +33,14 @@ const HelpModal = React.lazy(() => import("../../components/modal/HelpModal"));
|
||||||
|
|
||||||
const NetworkSidebar = () => {
|
const NetworkSidebar = () => {
|
||||||
const {t, i18n} = useTranslation();
|
const {t, i18n} = useTranslation();
|
||||||
|
const {authRequired} = useAuth();
|
||||||
const {sidebarItems, setSidebarItems} = useNavigation();
|
const {sidebarItems, setSidebarItems} = useNavigation();
|
||||||
const {networkMetadata} = useMetadata();
|
const {networkMetadata} = useMetadata();
|
||||||
const {extensionData} = useServerExtensionContext();
|
const {extensionData} = useServerExtensionContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const servers = networkMetadata?.servers || [];
|
const servers = networkMetadata?.servers || [];
|
||||||
const items = [
|
let items = [
|
||||||
{
|
{
|
||||||
name: 'html.label.networkOverview',
|
name: 'html.label.networkOverview',
|
||||||
icon: faInfoCircle,
|
icon: faInfoCircle,
|
||||||
|
@ -119,6 +121,13 @@ const NetworkSidebar = () => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{name: 'html.label.plugins', permission: 'page.network.plugins'},
|
{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',
|
name: 'html.label.pluginsOverview',
|
||||||
icon: faCubes,
|
icon: faCubes,
|
||||||
|
@ -147,10 +156,13 @@ const NetworkSidebar = () => {
|
||||||
{name: 'html.label.query', icon: faSearch, href: "/query", permission: 'access.query'}
|
{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);
|
setSidebarItems(items);
|
||||||
window.document.title = `Plan | Network`;
|
window.document.title = `Plan | Network`;
|
||||||
}, [t, i18n, extensionData, setSidebarItems, networkMetadata])
|
}, [t, i18n, extensionData, setSidebarItems, networkMetadata, authRequired])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar items={sidebarItems}/>
|
<Sidebar items={sidebarItems}/>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
faCampground,
|
faCampground,
|
||||||
faChartArea,
|
faChartArea,
|
||||||
faChartLine,
|
faChartLine,
|
||||||
|
faCodeCompare,
|
||||||
faCogs,
|
faCogs,
|
||||||
faCubes,
|
faCubes,
|
||||||
faGlobe,
|
faGlobe,
|
||||||
|
@ -35,11 +36,12 @@ const HelpModal = React.lazy(() => import("../../components/modal/HelpModal"));
|
||||||
|
|
||||||
const ServerSidebar = () => {
|
const ServerSidebar = () => {
|
||||||
const {t, i18n} = useTranslation();
|
const {t, i18n} = useTranslation();
|
||||||
|
const {authRequired} = useAuth();
|
||||||
const {sidebarItems, setSidebarItems} = useNavigation();
|
const {sidebarItems, setSidebarItems} = useNavigation();
|
||||||
const {extensionData} = useServerExtensionContext();
|
const {extensionData} = useServerExtensionContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const items = [
|
let items = [
|
||||||
{
|
{
|
||||||
name: 'html.label.serverOverview',
|
name: 'html.label.serverOverview',
|
||||||
icon: faInfoCircle,
|
icon: faInfoCircle,
|
||||||
|
@ -113,6 +115,13 @@ const ServerSidebar = () => {
|
||||||
{name: 'html.label.performance', icon: faCogs, href: "performance", permission: 'page.server.performance'},
|
{name: 'html.label.performance', icon: faCogs, href: "performance", permission: 'page.server.performance'},
|
||||||
{},
|
{},
|
||||||
{name: 'html.label.plugins', permission: 'page.server.plugins'},
|
{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',
|
name: 'html.label.pluginsOverview',
|
||||||
icon: faCubes,
|
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);
|
setSidebarItems(items);
|
||||||
window.document.title = `Plan | Server Analysis`;
|
window.document.title = `Plan | Server Analysis`;
|
||||||
}, [t, i18n, extensionData, setSidebarItems])
|
}, [t, i18n, extensionData, setSidebarItems, authRequired])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar items={sidebarItems}/>
|
<Sidebar items={sidebarItems}/>
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
import {Col, InputGroup} from "react-bootstrap";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import LoadIn from "../../components/animation/LoadIn";
|
||||||
|
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||||
|
import {useAuth} from "../../hooks/authenticationHook";
|
||||||
|
import PluginHistoryCard from "../../components/cards/common/PluginHistoryCard";
|
||||||
|
import {useDataRequest} from "../../hooks/dataFetchHook";
|
||||||
|
import {fetchPluginHistory} from "../../service/serverService";
|
||||||
|
import PluginCurrentCard from "../../components/cards/common/PluginCurrentCard";
|
||||||
|
import {useMetadata} from "../../hooks/metadataHook";
|
||||||
|
import Select from "../../components/input/Select";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
import InputGroupText from "react-bootstrap/InputGroupText";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faServer} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
const NetworkPluginHistory = () => {
|
||||||
|
const {t} = useTranslation();
|
||||||
|
const {authRequired, hasPermission} = useAuth();
|
||||||
|
|
||||||
|
const seeHistory = authRequired && hasPermission('page.network.plugin.history');
|
||||||
|
|
||||||
|
const {networkMetadata} = useMetadata();
|
||||||
|
const [serverOptions, setServerOptions] = useState([]);
|
||||||
|
const [selectedOption, setSelectedOption] = useState(0);
|
||||||
|
const [identifier, setIdentifier] = useState(undefined);
|
||||||
|
useEffect(() => {
|
||||||
|
if (networkMetadata) {
|
||||||
|
const options = networkMetadata.servers;
|
||||||
|
setServerOptions(options);
|
||||||
|
|
||||||
|
const indexOfProxy = options
|
||||||
|
.findIndex(option => option.serverName === networkMetadata.currentServer.serverName);
|
||||||
|
|
||||||
|
setSelectedOption(indexOfProxy);
|
||||||
|
}
|
||||||
|
}, [networkMetadata, setSelectedOption, setServerOptions]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (serverOptions.length) {
|
||||||
|
setIdentifier(serverOptions[selectedOption].serverUUID);
|
||||||
|
}
|
||||||
|
}, [selectedOption, serverOptions])
|
||||||
|
|
||||||
|
let {data, loadingError} = useDataRequest(fetchPluginHistory, [identifier], Boolean(identifier) && seeHistory);
|
||||||
|
if (!identifier) data = {history: []};
|
||||||
|
return (
|
||||||
|
<LoadIn>
|
||||||
|
{seeHistory && <section id="network-plugin-history">
|
||||||
|
<ExtendableRow id={'row-network-plugin-history-0'}>
|
||||||
|
<Col md={4} className={"mb-4"}>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupText><FontAwesomeIcon icon={faServer}/> {t('html.label.serverSelector')}
|
||||||
|
</InputGroupText>
|
||||||
|
<Select options={serverOptions.map(server => server.serverName)}
|
||||||
|
selectedIndex={selectedOption} setSelectedIndex={setSelectedOption}/>
|
||||||
|
</InputGroup>
|
||||||
|
</Col>
|
||||||
|
</ExtendableRow>
|
||||||
|
<ExtendableRow id={'row-network-plugin-history-1'}>
|
||||||
|
<Col md={6}>
|
||||||
|
<PluginCurrentCard data={data} loadingError={loadingError}/>
|
||||||
|
</Col>
|
||||||
|
<Col md={6}>
|
||||||
|
<PluginHistoryCard data={data} loadingError={loadingError}/>
|
||||||
|
</Col>
|
||||||
|
</ExtendableRow>
|
||||||
|
</section>}
|
||||||
|
</LoadIn>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NetworkPluginHistory;
|
|
@ -0,0 +1,34 @@
|
||||||
|
import {Col} from "react-bootstrap";
|
||||||
|
import React from "react";
|
||||||
|
import LoadIn from "../../components/animation/LoadIn";
|
||||||
|
import {useParams} from "react-router-dom";
|
||||||
|
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||||
|
import {useAuth} from "../../hooks/authenticationHook";
|
||||||
|
import PluginHistoryCard from "../../components/cards/common/PluginHistoryCard";
|
||||||
|
import {useDataRequest} from "../../hooks/dataFetchHook";
|
||||||
|
import {fetchPluginHistory} from "../../service/serverService";
|
||||||
|
import PluginCurrentCard from "../../components/cards/common/PluginCurrentCard";
|
||||||
|
|
||||||
|
const ServerPluginHistory = () => {
|
||||||
|
const {authRequired, hasPermission} = useAuth();
|
||||||
|
const {identifier} = useParams();
|
||||||
|
|
||||||
|
const seeHistory = authRequired && hasPermission('page.server.plugin.history');
|
||||||
|
const {data, loadingError} = useDataRequest(fetchPluginHistory, [identifier], seeHistory);
|
||||||
|
return (
|
||||||
|
<LoadIn>
|
||||||
|
{seeHistory && <section id="server-plugin-history">
|
||||||
|
<ExtendableRow id={'row-server-plugin-history-0'}>
|
||||||
|
<Col md={6}>
|
||||||
|
<PluginCurrentCard data={data} loadingError={loadingError}/>
|
||||||
|
</Col>
|
||||||
|
<Col md={6}>
|
||||||
|
<PluginHistoryCard data={data} loadingError={loadingError}/>
|
||||||
|
</Col>
|
||||||
|
</ExtendableRow>
|
||||||
|
</section>}
|
||||||
|
</LoadIn>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServerPluginHistory;
|
|
@ -16,10 +16,12 @@
|
||||||
*/
|
*/
|
||||||
package com.djrapitops.plan.gathering;
|
package com.djrapitops.plan.gathering;
|
||||||
|
|
||||||
|
import com.djrapitops.plan.gathering.domain.PluginMetadata;
|
||||||
import org.spongepowered.api.Game;
|
import org.spongepowered.api.Game;
|
||||||
import org.spongepowered.api.entity.living.player.server.ServerPlayer;
|
import org.spongepowered.api.entity.living.player.server.ServerPlayer;
|
||||||
import org.spongepowered.api.world.chunk.WorldChunk;
|
import org.spongepowered.api.world.chunk.WorldChunk;
|
||||||
import org.spongepowered.api.world.server.ServerWorld;
|
import org.spongepowered.api.world.server.ServerWorld;
|
||||||
|
import org.spongepowered.plugin.PluginContainer;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
@ -82,4 +84,14 @@ public class SpongeSensor implements ServerSensor<ServerWorld> {
|
||||||
public List<String> getOnlinePlayerNames() {
|
public List<String> getOnlinePlayerNames() {
|
||||||
return game.server().onlinePlayers().stream().map(ServerPlayer::name).collect(Collectors.toList());
|
return game.server().onlinePlayers().stream().map(ServerPlayer::name).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PluginMetadata> getInstalledPlugins() {
|
||||||
|
return game.pluginManager().plugins().stream()
|
||||||
|
.map(PluginContainer::metadata)
|
||||||
|
.map(metadata -> new PluginMetadata(
|
||||||
|
metadata.name().orElse(metadata.id()),
|
||||||
|
metadata.version().toString()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
|
||||||
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
|
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
|
||||||
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
|
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
|
||||||
import com.djrapitops.plan.gathering.ShutdownHook;
|
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.ServerTPSCounter;
|
||||||
import com.djrapitops.plan.gathering.timed.SpongePingCounter;
|
import com.djrapitops.plan.gathering.timed.SpongePingCounter;
|
||||||
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
||||||
|
@ -98,4 +99,8 @@ public interface SpongeTaskModule {
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,10 @@
|
||||||
package com.djrapitops.plan.gathering;
|
package com.djrapitops.plan.gathering;
|
||||||
|
|
||||||
import com.djrapitops.plan.PlanVelocity;
|
import com.djrapitops.plan.PlanVelocity;
|
||||||
|
import com.djrapitops.plan.gathering.domain.PluginMetadata;
|
||||||
import com.djrapitops.plan.identification.properties.VelocityRedisCheck;
|
import com.djrapitops.plan.identification.properties.VelocityRedisCheck;
|
||||||
import com.djrapitops.plan.identification.properties.VelocityRedisPlayersOnlineSupplier;
|
import com.djrapitops.plan.identification.properties.VelocityRedisPlayersOnlineSupplier;
|
||||||
|
import com.velocitypowered.api.plugin.PluginContainer;
|
||||||
import com.velocitypowered.api.proxy.Player;
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
@ -34,6 +36,7 @@ public class VelocitySensor implements ServerSensor<Object> {
|
||||||
|
|
||||||
private final IntSupplier onlinePlayerCountSupplier;
|
private final IntSupplier onlinePlayerCountSupplier;
|
||||||
private final Supplier<Collection<Player>> getPlayers;
|
private final Supplier<Collection<Player>> getPlayers;
|
||||||
|
private final Supplier<Collection<PluginContainer>> getPlugins;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public VelocitySensor(PlanVelocity plugin) {
|
public VelocitySensor(PlanVelocity plugin) {
|
||||||
|
@ -41,6 +44,7 @@ public class VelocitySensor implements ServerSensor<Object> {
|
||||||
onlinePlayerCountSupplier = VelocityRedisCheck.isClassAvailable()
|
onlinePlayerCountSupplier = VelocityRedisCheck.isClassAvailable()
|
||||||
? new VelocityRedisPlayersOnlineSupplier()
|
? new VelocityRedisPlayersOnlineSupplier()
|
||||||
: plugin.getProxy()::getPlayerCount;
|
: plugin.getProxy()::getPlayerCount;
|
||||||
|
getPlugins = () -> plugin.getProxy().getPluginManager().getPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -62,4 +66,14 @@ public class VelocitySensor implements ServerSensor<Object> {
|
||||||
public boolean usingRedisBungee() {
|
public boolean usingRedisBungee() {
|
||||||
return VelocityRedisCheck.isClassAvailable();
|
return VelocityRedisCheck.isClassAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PluginMetadata> getInstalledPlugins() {
|
||||||
|
return getPlugins.get().stream()
|
||||||
|
.map(PluginContainer::getDescription)
|
||||||
|
.map(description -> new PluginMetadata(
|
||||||
|
description.getName().orElse(description.getId()),
|
||||||
|
description.getVersion().orElse("html.label.installed")))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieExpiryCleanupTask
|
||||||
import com.djrapitops.plan.delivery.webserver.cache.JSONFileStorage;
|
import com.djrapitops.plan.delivery.webserver.cache.JSONFileStorage;
|
||||||
import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
|
import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
|
||||||
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
|
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
|
||||||
|
import com.djrapitops.plan.gathering.timed.InstalledPluginGatheringTask;
|
||||||
import com.djrapitops.plan.gathering.timed.ProxyTPSCounter;
|
import com.djrapitops.plan.gathering.timed.ProxyTPSCounter;
|
||||||
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
|
||||||
import com.djrapitops.plan.gathering.timed.VelocityPingCounter;
|
import com.djrapitops.plan.gathering.timed.VelocityPingCounter;
|
||||||
|
@ -87,4 +88,8 @@ public interface VelocityTaskModule {
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue