This commit is contained in:
AuroraLS3 2022-09-12 05:05:01 +00:00
parent b029dc6c7a
commit 8b85936912
72 changed files with 2116 additions and 344 deletions

View File

@ -276,6 +276,8 @@ html:
afkTime: "挂机时间"
all: "全部"
allTime: "所有时间"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "数据"
average: "平均"
averageActivePlaytime: "平均活跃时间"
@ -289,9 +291,11 @@ html:
averagePlayers: "Average Players"
averagePlaytime: "平均游玩时间"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "平均会话时长"
averageSessions: "平均会话"
averageTps: "平均 TPS"
averageTps7days: "Average TPS (7 days)"
banned: "已被封禁"
bestPeak: "所有时间峰值"
bestPing: "最低延迟"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "地理位置"
hourByHour: "按小时查看"
inactive: "不活跃"
@ -337,6 +347,7 @@ html:
lastConnected: "最后连接时间"
lastPeak: "上次在线峰值"
lastSeen: "最后在线时间"
latestJoinAddresses: "Latest Join Addresses"
length: " 游玩时长"
links: "链接"
loadedChunks: "已加载区块"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "单独新玩家加入"
longestSession: "最长会话时间"
lowTpsSpikes: "低 TPS 时间"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "最大可用硬盘空间"
medianSessionLength: "Median Session Length"
minFreeDisk: "最小可用硬盘空间"
@ -362,6 +374,7 @@ html:
new: "新"
newPlayerRetention: "新玩家留坑率"
newPlayers: "新玩家"
newPlayers7days: "New Players (7 days)"
nickname: "昵称"
noDataToDisplay: "No Data to Display"
now: "现在"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "玩家"
playersOnline: "在线玩家"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "在线活动总览"
playtime: "游玩时间"
plugins: "插件"
@ -421,6 +435,7 @@ html:
serverPage: "服务器页面"
serverPlaytime: "服务器游戏时间"
serverPlaytime30days: "最近 30 天内的服务器游玩时间"
serverSelector: "Server selector"
servers: "服务器"
serversTitle: "服务器"
session: "会话次数"
@ -429,21 +444,26 @@ html:
sessionMedian: "平均会话长度"
sessionStart: "会话开始于"
sessions: "会话"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "主题选择"
thirdDeadliestWeapon: "第三致命的 PVP 武器"
thirtyDays: "30 天"
thirtyDaysAgo: "30 天前"
timesKicked: "被踢出次数"
toMainPage: "回到主页面"
total: "Total"
totalActive: "总活跃时长"
totalAfk: "总挂机时长"
totalPlayers: "总玩家数"
totalPlayersOld: "总游玩时长"
totalPlaytime: "总游玩时间"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "趋势"
trends30days: "30 天趋势"
uniquePlayers: "独立玩家"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "非常活跃"
weekComparison: "每周对比"
weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'"

View File

@ -269,29 +269,33 @@ html:
generic:
none: "Žádný"
label:
active: "Aktivní"
activePlaytime: "Aktivní herní čas"
activityIndex: "Index aktivity"
afk: "AFK"
afkTime: "AFK čas"
all: "Vše"
allTime: "Celkově"
asNumbers: "statistiky"
average: "Průměr"
averageActivePlaytime: "Průměrná herní aktivita"
averageAfkTime: "Průměrný AFK čas"
averageChunks: "Průměr chunků"
averageCpuUsage: "Average CPU Usage"
averageEntities: "Průměr entit"
averageKdr: "Průměr KDR"
averageMobKdr: "Průměr Mob KDR"
averagePing: "Průměrný ping"
active: "Aktivní"
activePlaytime: "Aktivní herní čas"
activityIndex: "Index aktivity"
afk: "AFK"
afkTime: "AFK čas"
all: "Vše"
allTime: "Celkově"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "statistiky"
average: "Průměr"
averageActivePlaytime: "Průměrná herní aktivita"
averageAfkTime: "Průměrný AFK čas"
averageChunks: "Průměr chunků"
averageCpuUsage: "Average CPU Usage"
averageEntities: "Průměr entit"
averageKdr: "Průměr KDR"
averageMobKdr: "Průměr Mob KDR"
averagePing: "Průměrný ping"
averagePlayers: "Average Players"
averagePlaytime: "Průměr herní doby"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "Průměrná délka relace"
averageSessions: "Průměrná relace"
averageTps: "Průměr TPS"
averageTps7days: "Average TPS (7 days)"
banned: "Zabanován"
bestPeak: "Nejvíce hráčů"
bestPing: "Nejlepší ping"
@ -308,26 +312,32 @@ html:
dayOfweek: "Den týdne"
deadliestWeapon: "Nejsmrtelnější PvP zbraň"
deaths: "Smrti"
disk: "Místo na disku"
diskSpace: "Volné místo na disku"
downtime: "Offline doba"
duringLowTps: "Při nízkých TPS:"
entities: "Entity"
favoriteServer: "Oblíbený server"
firstSession: "První relace"
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geolocations: "Geolokace"
hourByHour: "Hodina po hodině"
inactive: "Neaktivní"
indexInactive: "Neaktivní"
indexRegular: "Pravidelný"
information: "INFORMACE"
insights: "Insights"
insights30days: "Postřehy za 30 dní"
irregular: "Nepravidelný"
joinAddress: "Join Address"
disk: "Místo na disku"
diskSpace: "Volné místo na disku"
downtime: "Offline doba"
duringLowTps: "Při nízkých TPS:"
entities: "Entity"
favoriteServer: "Oblíbený server"
firstSession: "První relace"
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "Geolokace"
hourByHour: "Hodina po hodině"
inactive: "Neaktivní"
indexInactive: "Neaktivní"
indexRegular: "Pravidelný"
information: "INFORMACE"
insights: "Insights"
insights30days: "Postřehy za 30 dní"
irregular: "Nepravidelný"
joinAddress: "Join Address"
joinAddresses: "Připojovací IP"
kdr: "KDR"
killed: "Zabit"
@ -337,6 +347,7 @@ html:
lastConnected: "Poslední připojení"
lastPeak: "Naposledy nejvíce hráčů"
lastSeen: "Naposledy viděn"
latestJoinAddresses: "Latest Join Addresses"
length: " Délka"
links: "ODKAZY"
loadedChunks: "Načtené chunky"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "Samotná připojení nováčků"
longestSession: "Nejdelší relace"
lowTpsSpikes: "Nejnižší TPS"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "Max. volného disku"
medianSessionLength: "Median Session Length"
minFreeDisk: "Min. volného disku"
@ -362,6 +374,7 @@ html:
new: "Nový"
newPlayerRetention: "Udržení nových hráčů"
newPlayers: "Noví hráči"
newPlayers7days: "New Players (7 days)"
nickname: "Přezdívka"
noDataToDisplay: "No Data to Display"
now: "Nyní"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "Hráči"
playersOnline: "Hráči online"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Přehled online aktivity"
playtime: "Herní čas"
plugins: "Pluginy"
@ -420,30 +434,36 @@ html:
serverOverview: "Přehled serveru"
serverPage: "Stránka serveru"
serverPlaytime: "Herní čas serveru"
serverPlaytime30days: "Herní čas serveru za 30 dní"
servers: "Servery"
serversTitle: "SERVERY"
session: "Relace"
sessionCalendar: "Session Calendar"
sessionEnded: " Ukončeno"
sessionMedian: "Střední hodnota relací"
sessionStart: "Započatá relace"
sessions: "Relace"
themeSelect: "Zvolené téma"
thirdDeadliestWeapon: "3. PvP Zbraň"
thirtyDays: "30 dní"
thirtyDaysAgo: "před 30 dny"
timesKicked: "Počet vykopnutí"
toMainPage: "Zpět na hlavní stránku"
totalActive: "Celková aktivita"
totalAfk: "Celkem AFK"
totalPlayers: "Hráčů celkem"
serverPlaytime30days: "Herní čas serveru za 30 dní"
serverSelector: "Server selector"
servers: "Servery"
serversTitle: "SERVERY"
session: "Relace"
sessionCalendar: "Session Calendar"
sessionEnded: " Ukončeno"
sessionMedian: "Střední hodnota relací"
sessionStart: "Započatá relace"
sessions: "Relace"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "Zvolené téma"
thirdDeadliestWeapon: "3. PvP Zbraň"
thirtyDays: "30 dní"
thirtyDaysAgo: "před 30 dny"
timesKicked: "Počet vykopnutí"
toMainPage: "Zpět na hlavní stránku"
total: "Total"
totalActive: "Celková aktivita"
totalAfk: "Celkem AFK"
totalPlayers: "Hráčů celkem"
totalPlayersOld: "Celkem hráčů"
totalPlaytime: "Herní doba celkem"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "Trend"
trends30days: "Trendy za 30 dní"
uniquePlayers: "Unikátní hráči"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "Velmi aktivní"
weekComparison: "Týdenní srovnání"
weekdays: "'Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek', 'Sobota', 'Neděle'"

View File

@ -276,6 +276,8 @@ html:
afkTime: "AFK Zeit"
all: "Gesamt"
allTime: "Gesamte zeit"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "als Zahlen"
average: "Durchschnitt"
averageActivePlaytime: "Durchschnittliche aktive Spielzeit"
@ -289,9 +291,11 @@ html:
averagePlayers: "Average Players"
averagePlaytime: "Durschnittliche Spielzeit"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "Durschnittliche Sitzungslänge"
averageSessions: "Durchschnittliche Sessions"
averageTps: "Durschnittliche TPS"
averageTps7days: "Average TPS (7 days)"
banned: "Gebannt"
bestPeak: "Rekord"
bestPing: "Bester Ping"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "Geolocations"
hourByHour: "Hour by Hour"
inactive: "Inaktiv"
@ -337,6 +347,7 @@ html:
lastConnected: "Letzte Verbindung"
lastPeak: "Letzter Höchststand"
lastSeen: "Zuletzt gesehen"
latestJoinAddresses: "Latest Join Addresses"
length: " Länge"
links: "LINKS"
loadedChunks: "Geladene Chunks"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "Lone newbie joins"
longestSession: "Längste Sitzung"
lowTpsSpikes: "Low TPS Spitzen"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "Max Freier Speicher"
medianSessionLength: "Median Session Length"
minFreeDisk: "Min Freier Speicher"
@ -362,6 +374,7 @@ html:
new: "Neu"
newPlayerRetention: "Erhaltung neuer Spieler"
newPlayers: "Neue Spieler"
newPlayers7days: "New Players (7 days)"
nickname: "Nickname"
noDataToDisplay: "No Data to Display"
now: "Jetzt"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "Spieler"
playersOnline: "Spieler Online"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Online Aktivitätsübersicht"
playtime: "Spielzeit"
plugins: "Plugins"
@ -421,6 +435,7 @@ html:
serverPage: "Server Seite"
serverPlaytime: "Server Spielzeit"
serverPlaytime30days: "Server Spielzeit für 30 Tage"
serverSelector: "Server selector"
servers: "Server"
serversTitle: "SERVER"
session: "Sitzung"
@ -429,21 +444,26 @@ html:
sessionMedian: "Sitzungsdurchschnitt"
sessionStart: "Sitzung gestartet"
sessions: "Sitzungen"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "Thema ausgewählt"
thirdDeadliestWeapon: "3. PvP Waffe"
thirtyDays: "30 Tage"
thirtyDaysAgo: "30 Tage vorher"
timesKicked: "Mal gekickt"
toMainPage: "zur Hauptseite"
total: "Total"
totalActive: "Gesamte Aktive Spielzeit"
totalAfk: "Gesamte AFK-Zeit"
totalPlayers: "Gesamte Spieler"
totalPlayersOld: "Gesamte Spieler"
totalPlaytime: "Gesamte Spielzeit"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "Trend"
trends30days: "Trends für 30 Tage"
uniquePlayers: "Einzigartige Spieler"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "Sehr aktiv"
weekComparison: "Wochenvergleich"
weekdays: "'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'"

View File

@ -269,29 +269,33 @@ html:
generic:
none: "None"
label:
active: "Active"
activePlaytime: "Active Playtime"
activityIndex: "Activity Index"
afk: "AFK"
afkTime: "AFK Time"
all: "All"
allTime: "All Time"
asNumbers: "as Numbers"
average: "Average"
averageActivePlaytime: "Average Active Playtime"
averageAfkTime: "Average AFK Time"
averageChunks: "Average Chunks"
averageCpuUsage: "Average CPU Usage"
averageEntities: "Average Entities"
averageKdr: "Average KDR"
averageMobKdr: "Average Mob KDR"
averagePing: "Average Ping"
active: "Active"
activePlaytime: "Active Playtime"
activityIndex: "Activity Index"
afk: "AFK"
afkTime: "AFK Time"
all: "All"
allTime: "All Time"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "as Numbers"
average: "Average"
averageActivePlaytime: "Average Active Playtime"
averageAfkTime: "Average AFK Time"
averageChunks: "Average Chunks"
averageCpuUsage: "Average CPU Usage"
averageEntities: "Average Entities"
averageKdr: "Average KDR"
averageMobKdr: "Average Mob KDR"
averagePing: "Average Ping"
averagePlayers: "Average Players"
averagePlaytime: "Average Playtime"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "Average Session Length"
averageSessions: "Average Sessions"
averageTps: "Average TPS"
averageTps7days: "Average TPS (7 days)"
banned: "Banned"
bestPeak: "All Time Peak"
bestPing: "Best Ping"
@ -308,26 +312,32 @@ html:
dayOfweek: "Day of the Week"
deadliestWeapon: "Deadliest PvP Weapon"
deaths: "Deaths"
disk: "Disk space"
diskSpace: "Free Disk Space"
downtime: "Downtime"
duringLowTps: "During Low TPS Spikes:"
entities: "Entities"
favoriteServer: "Favorite Server"
firstSession: "First session"
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geolocations: "Geolocations"
hourByHour: "Hour by Hour"
inactive: "Inactive"
indexInactive: "Inactive"
indexRegular: "Regular"
information: "INFORMATION"
insights: "Insights"
insights30days: "Insights for 30 days"
irregular: "Irregular"
joinAddress: "Join Address"
disk: "Disk space"
diskSpace: "Free Disk Space"
downtime: "Downtime"
duringLowTps: "During Low TPS Spikes:"
entities: "Entities"
favoriteServer: "Favorite Server"
firstSession: "First session"
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "Geolocations"
hourByHour: "Hour by Hour"
inactive: "Inactive"
indexInactive: "Inactive"
indexRegular: "Regular"
information: "INFORMATION"
insights: "Insights"
insights30days: "Insights for 30 days"
irregular: "Irregular"
joinAddress: "Join Address"
joinAddresses: "Join Addresses"
kdr: "KDR"
killed: "Killed"
@ -337,6 +347,7 @@ html:
lastConnected: "Last Connected"
lastPeak: "Last Peak"
lastSeen: "Last Seen"
latestJoinAddresses: "Latest Join Addresses"
length: " Length"
links: "LINKS"
loadedChunks: "Loaded Chunks"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "Lone newbie joins"
longestSession: "Longest Session"
lowTpsSpikes: "Low TPS Spikes"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "Max Free Disk"
medianSessionLength: "Median Session Length"
minFreeDisk: "Min Free Disk"
@ -362,6 +374,7 @@ html:
new: "New"
newPlayerRetention: "New Player Retention"
newPlayers: "New Players"
newPlayers7days: "New Players (7 days)"
nickname: "Nickname"
noDataToDisplay: "No Data to Display"
now: "Now"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "Players"
playersOnline: "Players Online"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Online Activity Overview"
playtime: "Playtime"
plugins: "Plugins"
@ -420,30 +434,36 @@ html:
serverOverview: "Server Overview"
serverPage: "Server page"
serverPlaytime: "Server Playtime"
serverPlaytime30days: "Server Playtime for 30 days"
servers: "Servers"
serversTitle: "SERVERS"
session: "Session"
sessionCalendar: "Session Calendar"
sessionEnded: " Ended"
sessionMedian: "Session Median"
sessionStart: "Session Started"
sessions: "Sessions"
themeSelect: "Theme Select"
thirdDeadliestWeapon: "3rd PvP Weapon"
thirtyDays: "30 days"
thirtyDaysAgo: "30 days ago"
timesKicked: "Times Kicked"
toMainPage: "to main page"
totalActive: "Total Active"
totalAfk: "Total AFK"
totalPlayers: "Total Players"
serverPlaytime30days: "Server Playtime for 30 days"
serverSelector: "Server selector"
servers: "Servers"
serversTitle: "SERVERS"
session: "Session"
sessionCalendar: "Session Calendar"
sessionEnded: " Ended"
sessionMedian: "Session Median"
sessionStart: "Session Started"
sessions: "Sessions"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "Theme Select"
thirdDeadliestWeapon: "3rd PvP Weapon"
thirtyDays: "30 days"
thirtyDaysAgo: "30 days ago"
timesKicked: "Times Kicked"
toMainPage: "to main page"
total: "Total"
totalActive: "Total Active"
totalAfk: "Total AFK"
totalPlayers: "Total Players"
totalPlayersOld: "Total Players"
totalPlaytime: "Total Playtime"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "Trend"
trends30days: "Trends for 30 days"
uniquePlayers: "Unique Players"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "Very Active"
weekComparison: "Week Comparison"
weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"

View File

@ -269,29 +269,33 @@ html:
generic:
none: "Nada"
label:
active: "Activo"
activePlaytime: "Tiempo de juego activo"
activityIndex: "Índice de actividad"
afk: "AFK"
afkTime: "Tiempo AFK"
all: "Todo"
allTime: "Todo el tiempo"
asNumbers: "como números"
average: "Promedio"
averageActivePlaytime: "Tiempo de juego activo promedio"
averageAfkTime: "Tiempo AFK promedio"
averageChunks: "Chunks Promedio"
averageCpuUsage: "Average CPU Usage"
averageEntities: "Entidades promedio"
averageKdr: "KDR promedio"
averageMobKdr: "KDR de mobs promedio"
averagePing: "Ping promedio"
active: "Activo"
activePlaytime: "Tiempo de juego activo"
activityIndex: "Índice de actividad"
afk: "AFK"
afkTime: "Tiempo AFK"
all: "Todo"
allTime: "Todo el tiempo"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "como números"
average: "Promedio"
averageActivePlaytime: "Tiempo de juego activo promedio"
averageAfkTime: "Tiempo AFK promedio"
averageChunks: "Chunks Promedio"
averageCpuUsage: "Average CPU Usage"
averageEntities: "Entidades promedio"
averageKdr: "KDR promedio"
averageMobKdr: "KDR de mobs promedio"
averagePing: "Ping promedio"
averagePlayers: "Average Players"
averagePlaytime: "Jugabilidad promedio"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "Duración de sesion promedio"
averageSessions: "Sesiones promedio"
averageTps: "TPS promedio"
averageTps7days: "Average TPS (7 days)"
banned: "Baneado"
bestPeak: "Mejor pico"
bestPing: "Mejor Ping"
@ -308,26 +312,32 @@ html:
dayOfweek: "Dia de la semana"
deadliestWeapon: "Arma PvP más mortal"
deaths: "Muertes"
disk: "Espacio del disco"
diskSpace: "Espacio libre en el disco duro"
downtime: "Falta de tiempo"
duringLowTps: "Durante picos bajos de TPS:"
entities: "Entidades"
favoriteServer: "Servidor favorito"
firstSession: "Primera sesión"
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geolocations: "Geolocalizaciones"
hourByHour: "Hora a Hora"
inactive: "Inactivo"
indexInactive: "Inactivo"
indexRegular: "Regular"
information: "INFORMACIÓN"
insights: "Insights"
insights30days: "Ideas por 30 días"
irregular: "Irregular"
joinAddress: "Join Address"
disk: "Espacio del disco"
diskSpace: "Espacio libre en el disco duro"
downtime: "Falta de tiempo"
duringLowTps: "Durante picos bajos de TPS:"
entities: "Entidades"
favoriteServer: "Servidor favorito"
firstSession: "Primera sesión"
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "Geolocalizaciones"
hourByHour: "Hora a Hora"
inactive: "Inactivo"
indexInactive: "Inactivo"
indexRegular: "Regular"
information: "INFORMACIÓN"
insights: "Insights"
insights30days: "Ideas por 30 días"
irregular: "Irregular"
joinAddress: "Join Address"
joinAddresses: "Direcciones de entrada"
kdr: "KDR"
killed: "Muerto"
@ -337,6 +347,7 @@ html:
lastConnected: "Última vez conectado"
lastPeak: "Último pico"
lastSeen: "Última vez visto"
latestJoinAddresses: "Latest Join Addresses"
length: " Duración"
links: "LINKS"
loadedChunks: "Chunks cargados"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "Uniones solitarias nuevas"
longestSession: "Sesión más larga"
lowTpsSpikes: "Picos bajos TPS"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "Máximo espacio libre en el disco duro"
medianSessionLength: "Median Session Length"
minFreeDisk: "Mínimo espacio libre en el disco duro"
@ -362,6 +374,7 @@ html:
new: "Nuevo"
newPlayerRetention: "Retención nuevo jugador"
newPlayers: "Jugadores nuevos"
newPlayers7days: "New Players (7 days)"
nickname: "Nick"
noDataToDisplay: "No Data to Display"
now: "Ahora"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "Jugadores"
playersOnline: "Jugadores en línea"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Vista general de la actividad online"
playtime: "Tiempo de juego"
plugins: "Plugins"
@ -420,30 +434,36 @@ html:
serverOverview: "Vista general del servidor"
serverPage: "Página del servidor"
serverPlaytime: "Jugabilidad en números"
serverPlaytime30days: "Jugabilidad de 30 días"
servers: "Servidores"
serversTitle: "SERVERS"
session: "Sesión"
sessionCalendar: "Session Calendar"
sessionEnded: " Acabado"
sessionMedian: "Sesión media."
sessionStart: "Sesión iniciada"
sessions: "Sesiones"
themeSelect: "Selección de tema"
thirdDeadliestWeapon: "3ª arma PvP"
thirtyDays: "30 días"
thirtyDaysAgo: "Hace 30 días"
timesKicked: "Veces kickeado"
toMainPage: "hasta la página principal"
totalActive: "Activos totales"
totalAfk: "AFK total"
totalPlayers: "Jugadores totales"
serverPlaytime30days: "Jugabilidad de 30 días"
serverSelector: "Server selector"
servers: "Servidores"
serversTitle: "SERVERS"
session: "Sesión"
sessionCalendar: "Session Calendar"
sessionEnded: " Acabado"
sessionMedian: "Sesión media."
sessionStart: "Sesión iniciada"
sessions: "Sesiones"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "Selección de tema"
thirdDeadliestWeapon: "3ª arma PvP"
thirtyDays: "30 días"
thirtyDaysAgo: "Hace 30 días"
timesKicked: "Veces kickeado"
toMainPage: "hasta la página principal"
total: "Total"
totalActive: "Activos totales"
totalAfk: "AFK total"
totalPlayers: "Jugadores totales"
totalPlayersOld: "Jugadores totales"
totalPlaytime: "Jugabilidad total"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "Tendencia"
trends30days: "Tendencias de 30 días"
uniquePlayers: "Jugadores únicos"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "Muy activo"
weekComparison: "Comparación semanal"
weekdays: "'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'"

View File

@ -276,6 +276,8 @@ html:
afkTime: "Aika AFK:ina"
all: "Kaikki"
allTime: "Kaikkien aikojen"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "Numeroina"
average: "Keskimäräinen"
averageActivePlaytime: "Keskimäräinen Aktiivinen peliaika"
@ -289,9 +291,11 @@ html:
averagePlayers: "Average Players"
averagePlaytime: "Keskimäräinen peliaika"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "Keskimäräinen istunnon pituus"
averageSessions: "Keskimääräinen Sessiomäärä"
averageTps: "Keskimäräinen TPS"
averageTps7days: "Average TPS (7 days)"
banned: "Pannassa"
bestPeak: "Paras Huippu"
bestPing: "Paras Vasteaika"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "Sijainnit"
hourByHour: "Tunnittainen katsaus"
inactive: "Inaktiivinen"
@ -337,6 +347,7 @@ html:
lastConnected: "Viimeisin yhteys"
lastPeak: "Viimeisin huippu"
lastSeen: "Nähty Viimeksi"
latestJoinAddresses: "Latest Join Addresses"
length: " Pituus"
links: "LINKIT"
loadedChunks: "Ladatut Chunkit"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "Yksinäiset uusien pelaajien liittymiset"
longestSession: "Pisin istunto"
lowTpsSpikes: "Matalan TPS:n piikit"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "Maksimi vapaa levytila"
medianSessionLength: "Median Session Length"
minFreeDisk: "Minimi vapaa levytila"
@ -362,6 +374,7 @@ html:
new: "Uudet"
newPlayerRetention: "Uusien pysyvyys"
newPlayers: "Uusia pelaajia"
newPlayers7days: "New Players (7 days)"
nickname: "Lempinimi"
noDataToDisplay: "No Data to Display"
now: "Nyt"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "Pelaajia"
playersOnline: "Pelaajia Paikalla"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Yhteenveto Paikallaolosta"
playtime: "Peliaika"
plugins: "Lisäosat"
@ -421,6 +435,7 @@ html:
serverPage: "Palvelin"
serverPlaytime: "Palvelimen peliaika"
serverPlaytime30days: "Palvelimen peliaika 30 päivälle"
serverSelector: "Server selector"
servers: "Palvelimet"
serversTitle: "PALVELIMET"
session: "Istunto"
@ -429,21 +444,26 @@ html:
sessionMedian: "Istuntojen Mediaani"
sessionStart: "Istunto alkoi"
sessions: "Istunnot"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "Teemavalikko"
thirdDeadliestWeapon: "3. PvP Ase"
thirtyDays: "30 päivää"
thirtyDaysAgo: "30 päivää sitten"
timesKicked: "Heitetty pihalle"
toMainPage: "pääsivu"
total: "Total"
totalActive: "Aktiivisena"
totalAfk: "AFK"
totalPlayers: "Pelaajia"
totalPlayersOld: "Kaikki Pelaajat"
totalPlaytime: "Peliaikaa yhteensä"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "Suunta"
trends30days: "Suunnat 30 päivälle"
uniquePlayers: "Uniikkeja pelaajia"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "Todella Aktiivinen"
weekComparison: "Viikkojen vertaus"
weekdays: "'Maanantai', 'Tiistai', 'Keskiviikko', 'Torstai', 'Perjantai', 'Lauantai', 'Sunnuntai'"

View File

@ -276,6 +276,8 @@ html:
afkTime: "Temps AFK"
all: "Tout"
allTime: "Tout le Temps"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "en Chiffres"
average: "Moyen(ne)"
averageActivePlaytime: "Temps Actif moyen"
@ -289,9 +291,11 @@ html:
averagePlayers: "Average Players"
averagePlaytime: "Temps de Jeu moyen"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "Durée moyenne d'une Session"
averageSessions: "Quantité moyenne de Sessions"
averageTps: "TPS moyen"
averageTps7days: "Average TPS (7 days)"
banned: "Banni(e)"
bestPeak: "Pic maximal de Joueurs en Ligne"
bestPing: "Meilleure Latence"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "Géolocalisation"
hourByHour: "Heure par Heure"
inactive: "Inactif(ve)"
@ -337,6 +347,7 @@ html:
lastConnected: "Dernier Connecté"
lastPeak: "Dernier pic de Joueurs en Ligne"
lastSeen: "Dernière Connexion"
latestJoinAddresses: "Latest Join Addresses"
length: " Longueur"
links: "LIENS"
loadedChunks: "Chunks Chargés"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "Connexions de Débutants Seuls"
longestSession: "Session la plus Longue"
lowTpsSpikes: "Pics de TPS bas"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "Espace Disque MAX disponible"
medianSessionLength: "Median Session Length"
minFreeDisk: "Espace Disque MIN disponible"
@ -362,6 +374,7 @@ html:
new: "Nouveau(elle)"
newPlayerRetention: "Rétention des nouveaux Joueurs"
newPlayers: "Nouveaux Joueurs"
newPlayers7days: "New Players (7 days)"
nickname: "Surnom"
noDataToDisplay: "No Data to Display"
now: "Maintenant"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "Joueurs"
playersOnline: "Joueurs en Ligne"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Aperçu de l'Activité en Ligne"
playtime: "Temps de Jeu"
plugins: "Plugins"
@ -421,6 +435,7 @@ html:
serverPage: "Page du Serveur"
serverPlaytime: "Temps de Jeu du Serveur"
serverPlaytime30days: "Temps de Jeu du Serveur sur 30 Jours"
serverSelector: "Server selector"
servers: "Serveurs"
serversTitle: "SERVEURS"
session: "Session"
@ -429,21 +444,26 @@ html:
sessionMedian: "Session Médiane"
sessionStart: "Début de la Session"
sessions: "Sessions"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "Sélection du Thème"
thirdDeadliestWeapon: "3ᵉ Arme de Combat"
thirtyDays: "30 jours"
thirtyDaysAgo: "Il y a 30 jours"
timesKicked: "Nombre d'Éjections"
toMainPage: "Retour à la page principale"
total: "Total"
totalActive: "Temps Actif Total"
totalAfk: "Temps Inactif Total"
totalPlayers: "Joueurs Totaux"
totalPlayersOld: "Joueurs Totaux"
totalPlaytime: "Temps de Jeu Total"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "Tendances"
trends30days: "Tendances sur 30 Jours"
uniquePlayers: "Joueurs Uniques"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "Très Actif"
weekComparison: "Comparaison Hebdomadaire"
weekdays: "'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'"

View File

@ -276,6 +276,8 @@ html:
afkTime: "Tempo AFK"
all: "Tutto"
allTime: "Tutto il Tempo"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "Statistiche"
average: "Media"
averageActivePlaytime: "Average Active Playtime"
@ -289,9 +291,11 @@ html:
averagePlayers: "Average Players"
averagePlaytime: "Media Tempo di Gioco"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "Media Lunghezza Sessione"
averageSessions: "Average Sessions"
averageTps: "Media TPS"
averageTps7days: "Average TPS (7 days)"
banned: "Bannato"
bestPeak: "Record Migliore"
bestPing: "Ping Migliore"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "Geolocalizazione"
hourByHour: "Hour by Hour"
inactive: "Inattivo"
@ -337,6 +347,7 @@ html:
lastConnected: "Ultima connessione"
lastPeak: "Record Settimanale"
lastSeen: "Ultima Visita"
latestJoinAddresses: "Latest Join Addresses"
length: " Durata"
links: "LINKS"
loadedChunks: "Chunks Caricati"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "Nuove entrate Solitarie"
longestSession: "Sessione più lunga"
lowTpsSpikes: "Spicchi TPS bassi"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "Spazio Disco libero Max"
medianSessionLength: "Median Session Length"
minFreeDisk: "Spazio Disco libero Min"
@ -362,6 +374,7 @@ html:
new: "Nuovi"
newPlayerRetention: "Retenzione Nuovo Giocatore"
newPlayers: "Nuovi Giocatori"
newPlayers7days: "New Players (7 days)"
nickname: "Nick"
noDataToDisplay: "No Data to Display"
now: "Ora"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "Giocatori"
playersOnline: "Giocatori Online"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Panoramica delle attività online"
playtime: "Gioco"
plugins: "Plugins"
@ -421,6 +435,7 @@ html:
serverPage: "Pagina del Server"
serverPlaytime: "Tempo di Gioco Server"
serverPlaytime30days: "Tempo di Gioco Server di 30 giorni"
serverSelector: "Server selector"
servers: "Servers"
serversTitle: "SERVERS"
session: "Session"
@ -429,21 +444,26 @@ html:
sessionMedian: "Media Sessioni"
sessionStart: "Sessione Iniziata"
sessions: "Sessioni"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "Selezione Tema"
thirdDeadliestWeapon: "3° Arma PvP Preferita"
thirtyDays: "30 giorni"
thirtyDaysAgo: "30 giorni fa"
timesKicked: "Cacciato"
toMainPage: "Ritorna alla pagina principale"
total: "Total"
totalActive: "Totale Attivo"
totalAfk: "Totale AFK"
totalPlayers: "Giocatori Totali"
totalPlayersOld: "Totale Giocatori"
totalPlaytime: "Tempo di gioco Totale"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "Tendenza"
trends30days: "Tendenza per 30 giorni"
uniquePlayers: "Giocatori unici"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "Molto Attivo"
weekComparison: "Confronto settimanale"
weekdays: "'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica'"

View File

@ -276,6 +276,8 @@ html:
afkTime: "離席時間"
all: "全て"
allTime: "全体"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "の情報"
average: "平均"
averageActivePlaytime: "Average Active Playtime"
@ -289,9 +291,11 @@ html:
averagePlayers: "Average Players"
averagePlaytime: "平均プレイ時間"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "平均接続時間"
averageSessions: "Average Sessions"
averageTps: "平均TPS"
averageTps7days: "Average TPS (7 days)"
banned: "BAN履歴"
bestPeak: "全体のピークタイム"
bestPing: "最高Ping値"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "地域"
hourByHour: "Hour by Hour"
inactive: "休止中"
@ -337,6 +347,7 @@ html:
lastConnected: "直近の接続"
lastPeak: "直近のピークタイム"
lastSeen: "直近のオンライン"
latestJoinAddresses: "Latest Join Addresses"
length: " 長さ"
links: "LINKS"
loadedChunks: "ロードされたチャンク数"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "新しく一人での参加"
longestSession: "最長接続時間"
lowTpsSpikes: "TPSの低下値"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "ディスクの最大空き容量"
medianSessionLength: "Median Session Length"
minFreeDisk: "ディスクの最低空き容量"
@ -362,6 +374,7 @@ html:
new: "New"
newPlayerRetention: "新規プレイヤーの継続率"
newPlayers: "新規プレイヤー"
newPlayers7days: "New Players (7 days)"
nickname: "ニックネーム"
noDataToDisplay: "No Data to Display"
now: "現在"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "プレイヤー"
playersOnline: "オンラインのプレイヤー"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "接続状況の概要"
playtime: "プレイ時間"
plugins: "プラグイン"
@ -421,6 +435,7 @@ html:
serverPage: "サーバーページ"
serverPlaytime: "各サーバーのプレイ時間"
serverPlaytime30days: "各サーバーでの1ヶ月のプレイ時間"
serverSelector: "Server selector"
servers: "接続されているサーバー"
serversTitle: "接続されているサーバー"
session: "オンライン"
@ -429,21 +444,26 @@ html:
sessionMedian: "平均オンライン"
sessionStart: "接続した時間"
sessions: "接続履歴"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "テーマ選択"
thirdDeadliestWeapon: "3番目にPvPで使用されている武器"
thirtyDays: "1ヶ月"
thirtyDaysAgo: "1ヶ月前"
timesKicked: "キック回数"
toMainPage: "メインページに戻る"
total: "Total"
totalActive: "累計活動時間"
totalAfk: "累計離席時間"
totalPlayers: "トータルプレイヤー数"
totalPlayersOld: "全プレイヤー数"
totalPlaytime: "トータルプレイ時間"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "増減"
trends30days: "1ヶ月間の増減"
uniquePlayers: "接続したプレイヤーの総数"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "とてもログインしている"
weekComparison: "直近1周間での比較"
weekdays: "'月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'"

View File

@ -276,6 +276,8 @@ html:
afkTime: "AFK 시간"
all: "모두"
allTime: "모든 시간"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "숫자로"
average: "평균"
averageActivePlaytime: "Average Active Playtime"
@ -289,9 +291,11 @@ html:
averagePlayers: "Average Players"
averagePlaytime: "평균 플레이 타임"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "평균 접속 시간(세션 길이)"
averageSessions: "Average Sessions"
averageTps: "평균 TPS"
averageTps7days: "Average TPS (7 days)"
banned: "Banned"
bestPeak: "최고의 피크"
bestPing: "최고 Ping"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "지리적 위치"
hourByHour: "Hour by Hour"
inactive: "비활성"
@ -337,6 +347,7 @@ html:
lastConnected: "마지막 연결"
lastPeak: "마지막 피크"
lastSeen: "마지막으로 본"
latestJoinAddresses: "Latest Join Addresses"
length: " 길이"
links: "LINKS"
loadedChunks: "로드된 청크"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "최근 신규 접속(Lone New Joins)"
longestSession: "가장 긴 접속 시간"
lowTpsSpikes: "낮은 TPS 스파이크"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "최대 여유 디스크용량"
medianSessionLength: "Median Session Length"
minFreeDisk: "최소 여유 디스크용량"
@ -362,6 +374,7 @@ html:
new: "신규"
newPlayerRetention: "신규 플레이어 유지"
newPlayers: "신규 플레이어수"
newPlayers7days: "New Players (7 days)"
nickname: "닉네임"
noDataToDisplay: "No Data to Display"
now: "현재"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "플레이어들"
playersOnline: "접속중인 플레이어"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "온라인 활동 개요"
playtime: "플레이타임"
plugins: "플러그인"
@ -421,6 +435,7 @@ html:
serverPage: "서버 페이지"
serverPlaytime: "서버 플레이 타임"
serverPlaytime30days: "서버 플레이 타임 - 최근 30일"
serverSelector: "Server selector"
servers: "서버 목록"
serversTitle: "서버 목록"
session: "세션"
@ -429,21 +444,26 @@ html:
sessionMedian: "세션 중앙값"
sessionStart: "세션 시작"
sessions: "세션 목록"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "테마 선택"
thirdDeadliestWeapon: "3rd PvP 무기"
thirtyDays: "30일"
thirtyDaysAgo: "30일 전"
timesKicked: "접속종료한 시간"
toMainPage: "메인 페이지로"
total: "Total"
totalActive: "총 활성화 시간"
totalAfk: "총 AFK 시간"
totalPlayers: "총 플레이어수"
totalPlayersOld: "총 플레이어"
totalPlaytime: "총 플레이타임"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "트렌드"
trends30days: "30일 동안의 트렌드"
uniquePlayers: "기존 플레이어"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "매우 활성화된"
weekComparison: "주 비교"
weekdays: "'월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'"

View File

@ -276,6 +276,8 @@ html:
afkTime: "AFK Tijd"
all: "Alle"
allTime: "Alle Tijd"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "als nummers"
average: "Gemiddelde"
averageActivePlaytime: "Gemiddelde Actieve Speeltijd"
@ -289,9 +291,11 @@ html:
averagePlayers: "Average Players"
averagePlaytime: "Gemiddelde Speeltijd"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "Gemiddelde sessieduur"
averageSessions: "Gemiddelde sessies"
averageTps: "Gemiddelde TPS"
averageTps7days: "Average TPS (7 days)"
banned: "Verbannen"
bestPeak: "Piek aller tijden"
bestPing: "Beste ping"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "Geolocaties"
hourByHour: "Uur voor uur"
inactive: "Inactief"
@ -337,6 +347,7 @@ html:
lastConnected: "Laatst verbonden"
lastPeak: "Laatste piek"
lastSeen: "Laatste gezien"
latestJoinAddresses: "Latest Join Addresses"
length: " Lengte"
links: "LINKS"
loadedChunks: "Geladen Chunks"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "Eenzame nieuweling inloggen"
longestSession: "Langste sessie"
lowTpsSpikes: "Lage TPS-pieken"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "Max Vrije schijfruimte"
medianSessionLength: "Median Session Length"
minFreeDisk: "Min Vrije schijfruimte"
@ -362,6 +374,7 @@ html:
new: "Nieuw"
newPlayerRetention: "Retentie nieuwe speler"
newPlayers: "Nieuwe Spelers"
newPlayers7days: "New Players (7 days)"
nickname: "Gebruikersnaam"
noDataToDisplay: "No Data to Display"
now: "Nu"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "Spelers"
playersOnline: "Spelers Online"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Overzicht van online activiteiten"
playtime: "Speeltijd"
plugins: "Plugins"
@ -421,6 +435,7 @@ html:
serverPage: "Serverpagina"
serverPlaytime: "Server speeltijd"
serverPlaytime30days: "Server speeltijd voor 30 dagen"
serverSelector: "Server selector"
servers: "Servers"
serversTitle: "SERVERS"
session: "Sessie"
@ -429,21 +444,26 @@ html:
sessionMedian: "Sessiemediaan"
sessionStart: "Sessie gestart"
sessions: "Sessies"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "Thema selecteren"
thirdDeadliestWeapon: "3e PvP-wapen"
thirtyDays: "30 dagen"
thirtyDaysAgo: "30 dagen geleden"
timesKicked: "Aantal keer afgetapt"
toMainPage: "naar hoofdpagina"
total: "Total"
totalActive: "Totaal actief"
totalAfk: "Totaal AFK"
totalPlayers: "Totaal aantal spelers"
totalPlayersOld: "Totaal aantal spelers"
totalPlaytime: "Totale speeltijd"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "Trend"
trends30days: "Trends voor 30 dagen"
uniquePlayers: "Unieke spelers"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "Heel Actief"
weekComparison: "Weekvergelijking"
weekdays: "'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag', 'Zondag'"

View File

@ -276,6 +276,8 @@ html:
afkTime: "AFK Time"
all: "Todos"
allTime: "All Time"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "as Numbers"
average: "Average"
averageActivePlaytime: "Average Active Playtime"
@ -289,9 +291,11 @@ html:
averagePlayers: "Average Players"
averagePlaytime: "Average Playtime"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "Average Session Length"
averageSessions: "Average Sessions"
averageTps: "Average TPS"
averageTps7days: "Average TPS (7 days)"
banned: "Banido"
bestPeak: "Pico Máximo"
bestPing: "Best Ping"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "Geolocalizações"
hourByHour: "Hour by Hour"
inactive: "Inactive"
@ -337,6 +347,7 @@ html:
lastConnected: "Última Conexão"
lastPeak: "Último Pico"
lastSeen: "Última Vez Visto"
latestJoinAddresses: "Latest Join Addresses"
length: " Length"
links: "LINKS"
loadedChunks: "Chunks Carregados"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "Lone newbie joins"
longestSession: "Longest Session"
lowTpsSpikes: "Low TPS Spikes"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "Max Free Disk"
medianSessionLength: "Median Session Length"
minFreeDisk: "Min Free Disk"
@ -362,6 +374,7 @@ html:
new: "New"
newPlayerRetention: "Retenção de Novos Jogadores"
newPlayers: "Novos Jogadores"
newPlayers7days: "New Players (7 days)"
nickname: "Nick"
noDataToDisplay: "No Data to Display"
now: "Now"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "Jogadores"
playersOnline: "Jogadores Online"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Online Activity Overview"
playtime: "Tempo de Jogo"
plugins: "Plugins"
@ -421,6 +435,7 @@ html:
serverPage: "Server page"
serverPlaytime: "Server Playtime"
serverPlaytime30days: "Server Playtime for 30 days"
serverSelector: "Server selector"
servers: "Servidores"
serversTitle: "SERVERS"
session: "Sessão"
@ -429,21 +444,26 @@ html:
sessionMedian: "Média de Sessões"
sessionStart: "Session Started"
sessions: "Sessões"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "Theme Select"
thirdDeadliestWeapon: "3rd PvP Weapon"
thirtyDays: "30 days"
thirtyDaysAgo: "30 days ago"
timesKicked: "Vezes Kickado"
toMainPage: "to main page"
total: "Total"
totalActive: "Tempo Total Ativo"
totalAfk: "Tempo Total AFK"
totalPlayers: "Total Players"
totalPlayersOld: "Total de Jogadores"
totalPlaytime: "Total Playtime"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "Trend"
trends30days: "Trends for 30 days"
uniquePlayers: "Jogadores Únicos"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "Muito Ativo"
weekComparison: "Week Comparison"
weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"

View File

@ -276,6 +276,8 @@ html:
afkTime: "Время AFK"
all: "Все"
allTime: "Все время"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "В числах"
average: "Сред."
averageActivePlaytime: "Среднее время активной игры"
@ -289,9 +291,11 @@ html:
averagePlayers: "Среднее кол-во игроков"
averagePlaytime: "Среднее время игры"
averageRamUsage: "Среднее использование памяти"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "Средняя продолжительность сессии"
averageSessions: "Средняя сессия"
averageTps: "Средний TPS"
averageTps7days: "Average TPS (7 days)"
banned: "Забанен"
bestPeak: "Максимальный Пик"
bestPing: "Наилучший пинг"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Средняя продолжительность первого сеанса"
median: "Средняя продолжительность первого сеанса"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "Геолокация"
hourByHour: "Статистика по часам"
inactive: "Неактивный"
@ -337,6 +347,7 @@ html:
lastConnected: "Последнее подключение"
lastPeak: "Последний Пик"
lastSeen: "Последнее посещение"
latestJoinAddresses: "Latest Join Addresses"
length: " Длина"
links: "ССЫЛКИ"
loadedChunks: "Загруженные чанки"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "Одинокий новичок присоединяется"
longestSession: "Самая длинная сессия"
lowTpsSpikes: "Низкий TPS"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "Макс. свободный диск"
medianSessionLength: "Средняя продолжительность сеанса"
minFreeDisk: "Мин. свободный диск"
@ -362,6 +374,7 @@ html:
new: "Новый"
newPlayerRetention: "Сохранение нового игрока"
newPlayers: "Новые игроки"
newPlayers7days: "New Players (7 days)"
nickname: "Никнейм"
noDataToDisplay: "Нет данных для отображения"
now: "Сейчас"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Обзор базы игроков"
players: "Игроки"
playersOnline: "Игроки онлайн"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Обзор сетевой активности"
playtime: "Время игры"
plugins: "Плагины"
@ -421,6 +435,7 @@ html:
serverPage: "Страница сервера"
serverPlaytime: "Время игры на сервере"
serverPlaytime30days: "Время игры на сервере за 30 дней"
serverSelector: "Server selector"
servers: "Серверы"
serversTitle: "СЕРВЕРЫ"
session: "Сессия"
@ -429,21 +444,26 @@ html:
sessionMedian: "Средняя сессия"
sessionStart: "Сессия началась"
sessions: "Сессии"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "Выбор темы"
thirdDeadliestWeapon: "3-е PvP оружие"
thirtyDays: "30 дней"
thirtyDaysAgo: "30 дней назад"
timesKicked: "Кол-во киков"
toMainPage: "На главную страницу"
total: "Total"
totalActive: "Общая активность"
totalAfk: "Всего AFK"
totalPlayers: "Всего игроков"
totalPlayersOld: "Всего игроков"
totalPlaytime: "Общее время игры"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "Тенденция"
trends30days: "тенденция за 30 дней"
uniquePlayers: "Уникальные игроки"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "Очень активный"
weekComparison: "Сравнение за неделю"
weekdays: "'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'"

View File

@ -276,6 +276,8 @@ html:
afkTime: "AFK Süresi"
all: "Tamamı"
allTime: "Tüm zamanlar"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "Sayılar olarak"
average: "Ortalama"
averageActivePlaytime: "Ortalama Aktif Oyun Süresi"
@ -289,9 +291,11 @@ html:
averagePlayers: "Average Players"
averagePlaytime: "Ortalama Oyun Süresi"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "Ortalama Oturum Uzunluğu"
averageSessions: "Ortalama Oturumlar"
averageTps: "Ortalama TPS"
averageTps7days: "Average TPS (7 days)"
banned: "Yasaklanmış"
bestPeak: "Tüm Zamanların Zirvesi"
bestPing: "En iyi Ping"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "Coğrafi Konumlar"
hourByHour: "Saat saat"
inactive: "Etkin değil"
@ -337,6 +347,7 @@ html:
lastConnected: "Son bağlantı"
lastPeak: "Son Zirve"
lastSeen: "Son Görülme"
latestJoinAddresses: "Latest Join Addresses"
length: " Uzunluk"
links: "BAĞLANTILAR"
loadedChunks: "Yüklenmiş Chunks lar"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "Yalnız acemi katılıyor"
longestSession: "En Uzun Oturum"
lowTpsSpikes: "Low TPS Spikes"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "Maksimum Boş Disk"
medianSessionLength: "Median Session Length"
minFreeDisk: "Minimum Boş Disk"
@ -362,6 +374,7 @@ html:
new: "Yeni"
newPlayerRetention: "Yeni Oyuncu Elde Tutma"
newPlayers: "Yeni Oyuncular"
newPlayers7days: "New Players (7 days)"
nickname: "Takma ad"
noDataToDisplay: "No Data to Display"
now: "Şimdi"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "Oyuncular"
playersOnline: "Oyuncu Çevrimiçi"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "Çevrimiçi Etkinliğe Genel Bakış"
playtime: "Oyun Süresi"
plugins: "Pluginler"
@ -421,6 +435,7 @@ html:
serverPage: "Sunucu sayfası"
serverPlaytime: "Sunucu Oynatma Süresi"
serverPlaytime30days: "30 günlük Sunucu Oynatma Süresi"
serverSelector: "Server selector"
servers: "Sunucular"
serversTitle: "SUNUCULAR"
session: "Oturum"
@ -429,21 +444,26 @@ html:
sessionMedian: "Session Median"
sessionStart: "Oturum Başladı"
sessions: "Oturumlar"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "Tema Seçimi"
thirdDeadliestWeapon: "3. PvP Silahı"
thirtyDays: "30 gün"
thirtyDaysAgo: "30 gün önce"
timesKicked: "Kere Atılmış"
toMainPage: "Ana Sayfaya"
total: "Total"
totalActive: "Toplam Aktiflik"
totalAfk: "Toplam AFKlık"
totalPlayers: "Toplam Oyuncu"
totalPlayersOld: "Toplam Oyuncular"
totalPlaytime: "Toplam Oyun Süresi"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "Trend"
trends30days: "30 günlük trendler"
uniquePlayers: "Sunucuya İlk Defa Girenler"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "Çok Aktif"
weekComparison: "Hafta Karşılaştırması"
weekdays: "'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'"

View File

@ -276,6 +276,8 @@ html:
afkTime: "掛機時間"
all: "全部"
allTime: "所有時間"
alphabetical: "Alphabetical"
apply: "Apply"
asNumbers: "資料"
average: "平均"
averageActivePlaytime: "平均活躍時間"
@ -289,9 +291,11 @@ html:
averagePlayers: "Average Players"
averagePlaytime: "平均遊玩時間"
averageRamUsage: "Average RAM Usage"
averageServerDowntime: "Average Downtime / Server"
averageSessionLength: "平均會話時長"
averageSessions: "平均會話"
averageTps: "平均 TPS"
averageTps7days: "Average TPS (7 days)"
banned: "已被封禁"
bestPeak: "所有時間峰值"
bestPing: "最低延遲"
@ -318,6 +322,12 @@ html:
firstSessionLength:
average: "Average first session length"
median: "Median first session length"
geoProjection:
dropdown: "Select projection"
equalEarth: "Equal Earth"
mercator: "Mercator"
miller: "Miller"
ortographic: "Ortographic"
geolocations: "地理位置"
hourByHour: "按小時查看"
inactive: "不活躍"
@ -337,6 +347,7 @@ html:
lastConnected: "最後連接時間"
lastPeak: "上次線上峰值"
lastSeen: "最後線上時間"
latestJoinAddresses: "Latest Join Addresses"
length: " 遊玩時長"
links: "連接"
loadedChunks: "已載入區塊"
@ -345,6 +356,7 @@ html:
loneNewbieJoins: "單獨新玩家加入"
longestSession: "最長會話時間"
lowTpsSpikes: "低 TPS 時間"
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
maxFreeDisk: "最大可用硬碟空間"
medianSessionLength: "Median Session Length"
minFreeDisk: "最小可用硬碟空間"
@ -362,6 +374,7 @@ html:
new: "新"
newPlayerRetention: "新玩家留坑率"
newPlayers: "新玩家"
newPlayers7days: "New Players (7 days)"
nickname: "暱稱"
noDataToDisplay: "No Data to Display"
now: "現在"
@ -388,6 +401,7 @@ html:
playerbaseOverview: "Playerbase Overview"
players: "玩家"
playersOnline: "線上玩家"
playersOnlineNow: "Players Online (Now)"
playersOnlineOverview: "線上活動簡介"
playtime: "遊玩時間"
plugins: "插件"
@ -421,6 +435,7 @@ html:
serverPage: "伺服器頁面"
serverPlaytime: "伺服器遊戲時間"
serverPlaytime30days: "最近 30 天內的伺服器遊玩時間"
serverSelector: "Server selector"
servers: "伺服器"
serversTitle: "伺服器"
session: "會話次數"
@ -429,21 +444,26 @@ html:
sessionMedian: "平均會話長度"
sessionStart: "會話開始於"
sessions: "會話"
sortBy: "Sort By"
stacked: "Stacked"
themeSelect: "主題選擇"
thirdDeadliestWeapon: "第三致命的 PvP 武器"
thirtyDays: "30 天"
thirtyDaysAgo: "30 天前"
timesKicked: "被踢出次數"
toMainPage: "回到主頁面"
total: "Total"
totalActive: "總活躍時長"
totalAfk: "總掛機時長"
totalPlayers: "總玩家數"
totalPlayersOld: "總遊玩時長"
totalPlaytime: "總遊玩時間"
totalServerDowntime: "Total Server Downtime"
tps: "TPS"
trend: "趨勢"
trends30days: "30 天趨勢"
uniquePlayers: "獨立玩家"
uniquePlayers7days: "Unique Players (7 days)"
veryActive: "非常活躍"
weekComparison: "每週對比"
weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'"

View File

@ -1866,6 +1866,7 @@ a.text-dark:hover, a.text-dark:focus {
#wrapper {
display: flex;
min-height: 100vh;
}
#wrapper #content-wrapper {

View File

@ -19,7 +19,7 @@
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^14.4.3",
"axios": "^0.27.2",
"bootstrap": "^5.2.0",
"bootstrap": "^5.2.1",
"datatables.net": "^1.12.1",
"datatables.net-bs5": "^1.12.1",
"datatables.net-responsive-bs5": "^2.3.0",
@ -35,7 +35,7 @@
"react-i18next": "^11.18.5",
"react-router-dom": "6",
"react-scripts": "5.0.1",
"sass": "^1.54.8",
"sass": "^1.54.9",
"source-map-explorer": "^2.5.2",
"swagger-ui": "^4.14.0",
"web-vitals": "^3.0.1"

View File

@ -32,9 +32,16 @@ const LoginPage = React.lazy(() => import("./views/layout/LoginPage"));
const ServerPerformance = React.lazy(() => import("./views/server/ServerPerformance"));
const ServerPluginData = React.lazy(() => import("./views/server/ServerPluginData"));
const ServerWidePluginData = React.lazy(() => import("./views/server/ServerWidePluginData"));
const ServerJoinAddresses = React.lazy(() => import("./views/server/ServerJoinAddresses"));
const NetworkPage = React.lazy(() => import("./views/layout/NetworkPage"));
const NetworkOverview = React.lazy(() => import("./views/network/NetworkOverview"));
const NetworkServers = React.lazy(() => import("./views/network/NetworkServers"));
const NetworkSessions = React.lazy(() => import("./views/network/NetworkSessions"));
const NetworkJoinAddresses = React.lazy(() => import("./views/network/NetworkJoinAddresses"));
const NetworkGeolocations = React.lazy(() => import("./views/network/NetworkGeolocations"));
const NetworkPlayerbaseOverview = React.lazy(() => import("./views/network/NetworkPlayerbaseOverview"));
const NetworkPerformance = React.lazy(() => import("./views/network/NetworkPerformance"));
const PlayersPage = React.lazy(() => import("./views/layout/PlayersPage"));
const AllPlayers = React.lazy(() => import("./views/players/AllPlayers"));
@ -100,6 +107,7 @@ function App() {
<Route path="sessions" element={<Lazy><ServerSessions/></Lazy>}/>
<Route path="pvppve" element={<Lazy><ServerPvpPve/></Lazy>}/>
<Route path="playerbase" element={<Lazy><PlayerbaseOverview/></Lazy>}/>
<Route path="join-addresses" element={<Lazy><ServerJoinAddresses/></Lazy>}/>
<Route path="retention" element={<></>}/>
<Route path="players" element={<Lazy><ServerPlayers/></Lazy>}/>
<Route path="geolocations" element={<Lazy><ServerGeolocations/></Lazy>}/>
@ -115,7 +123,13 @@ function App() {
<Route path="/network" element={<Lazy><NetworkPage/></Lazy>}>
<Route path="" element={<Lazy><OverviewRedirect/></Lazy>}/>
<Route path="overview" element={<Lazy><NetworkOverview/></Lazy>}/>
<Route path="serversOverview" element={<Lazy><NetworkServers/></Lazy>}/>
<Route path="sessions" element={<Lazy><NetworkSessions/></Lazy>}/>
<Route path="performance" element={<Lazy><NetworkPerformance/></Lazy>}/>
<Route path="playerbase" element={<Lazy><NetworkPlayerbaseOverview/></Lazy>}/>
<Route path="join-addresses" element={<Lazy><NetworkJoinAddresses/></Lazy>}/>
<Route path="players" element={<Lazy><AllPlayers/></Lazy>}/>
<Route path="geolocations" element={<Lazy><NetworkGeolocations/></Lazy>}/>
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
<Route path="*" element={<ErrorView error={{

View File

@ -84,7 +84,8 @@ const SessionBody = ({i, session}) => {
const SessionAccordion = (
{
sessions,
isPlayer
isPlayer,
isNetwork
}
) => {
const {t} = useTranslation();
@ -99,7 +100,10 @@ const SessionAccordion = (
firstColumn,
<><Fa icon={faClock}/> {t('html.label.sessionStart')}</>,
<><Fa icon={faClock}/> {t('html.label.length')}</>,
<><Fa icon={faMap}/> {t('html.label.mostPlayedWorld')}</>
<>
{!isNetwork && <><Fa icon={faMap}/> {t('html.label.mostPlayedWorld')}</>}
{isNetwork && <><Fa icon={faServer}/> {t('html.label.server')}</>}
</>
]} slices={sessions.map(session => {
return {
body: <SessionBody session={session}/>,

View File

@ -1,22 +1,49 @@
import {useTranslation} from "react-i18next";
import {Card, Col, Row} from "react-bootstrap-v5";
import {Card, Col, Dropdown, Row} from "react-bootstrap-v5";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import React from "react";
import {faExclamationTriangle, faGlobe} from "@fortawesome/free-solid-svg-icons";
import React, {useState} from "react";
import {faExclamationTriangle, faGlobe, faLayerGroup} from "@fortawesome/free-solid-svg-icons";
import GeolocationBarGraph from "../../graphs/GeolocationBarGraph";
import GeolocationWorldMap from "../../graphs/GeolocationWorldMap";
import GeolocationWorldMap, {ProjectionOptions} from "../../graphs/GeolocationWorldMap";
import {CardLoader} from "../../navigation/Loader";
import DropdownToggle from "react-bootstrap-v5/lib/esm/DropdownToggle";
import DropdownMenu from "react-bootstrap-v5/lib/esm/DropdownMenu";
import DropdownItem from "react-bootstrap-v5/lib/esm/DropdownItem";
const ProjectionDropDown = ({projection, setProjection}) => {
const {t} = useTranslation();
const projectionOptions = Object.values(ProjectionOptions);
return (
<Dropdown className="float-end" style={{position: "absolute", right: "0.5rem"}}
title={t('html.label.geoProjection.dropdown')}>
<DropdownToggle variant=''>
<Fa icon={faLayerGroup}/> {t(projection)}
</DropdownToggle>
<DropdownMenu>
<h6 className="dropdown-header">{t('html.label.geoProjection.dropdown')}</h6>
{projectionOptions.map((option, i) => (
<DropdownItem key={i} onClick={() => setProjection(option)}>
{t(option)}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
)
}
const GeolocationsCard = ({data}) => {
const {t} = useTranslation();
const [projection, setProjection] = useState(ProjectionOptions.MILLER);
if (!data) return <CardLoader/>
if (!data?.geolocations_enabled) {
return (
<div className="alert alert-warning mb-0" id="geolocation-warning">
<Fa icon={faExclamationTriangle}/>{' '}
{t('html.description.noGeolocations')}
<Fa icon={faExclamationTriangle}/>{' '}{t('html.description.noGeolocations')}
</div>
)
}
@ -27,6 +54,7 @@ const GeolocationsCard = ({data}) => {
<h6 className="col-black">
<Fa icon={faGlobe} className="col-green"/> {t('html.label.geolocations')}
</h6>
<ProjectionDropDown projection={projection} setProjection={setProjection}/>
</Card.Header>
<Card.Body className="chart-area" style={{height: "100%"}}>
<Row>
@ -34,7 +62,8 @@ const GeolocationsCard = ({data}) => {
<GeolocationBarGraph series={data.geolocation_bar_series} color={data.colors.bars}/>
</Col>
<Col md={9}>
<GeolocationWorldMap series={data.geolocation_series} colors={data.colors}/>
<GeolocationWorldMap series={data.geolocation_series} colors={data.colors}
projection={projection}/>
</Col>
</Row>
</Card.Body>

View File

@ -6,7 +6,7 @@ import Scrollable from "../../Scrollable";
import SessionAccordion from "../../accordion/SessionAccordion";
import React from "react";
const RecentSessionsCard = ({sessions, isPlayer}) => {
const RecentSessionsCard = ({sessions, isPlayer, isNetwork}) => {
const {t} = useTranslation();
return (
<Card>
@ -19,7 +19,7 @@ const RecentSessionsCard = ({sessions, isPlayer}) => {
</h6>
</Card.Header>
<Scrollable>
<SessionAccordion sessions={sessions} isPlayer={isPlayer}/>
<SessionAccordion sessions={sessions} isPlayer={isPlayer} isNetwork={isNetwork}/>
</Scrollable>
</Card>
)

View File

@ -0,0 +1,28 @@
import {Card} from "react-bootstrap-v5";
import React from "react";
import {CardLoader} from "../../navigation/Loader";
import ServerPie from "../../graphs/ServerPie";
import {faNetworkWired} from "@fortawesome/free-solid-svg-icons";
import CardHeader from "../CardHeader";
import {useDataRequest} from "../../../hooks/dataFetchHook";
import {fetchServerPie} from "../../../service/networkService";
import {ErrorViewCard} from "../../../views/ErrorView";
const ServerPieCard = () => {
const {data, loadingError} = useDataRequest(fetchServerPie, []);
if (!data) return <CardLoader/>;
if (loadingError) return <ErrorViewCard error={loadingError}/>;
const series = data.server_pie_series_30d;
const colors = data.server_pie_colors;
return (
<Card>
<CardHeader icon={faNetworkWired} color={'teal'} label={'html.label.serverPlaytime30days'}/>
<ServerPie series={series} colors={colors}/>
</Card>
)
}
export default ServerPieCard;

View File

@ -0,0 +1,157 @@
import React from 'react';
import CardTabs from "../../CardTabs";
import {
faDragon,
faHdd,
faMap,
faMicrochip,
faSignal,
faTachometerAlt,
faUser
} from "@fortawesome/free-solid-svg-icons";
import {Card} from "react-bootstrap-v5";
import {useDataRequest} from "../../../hooks/dataFetchHook";
import {fetchPingGraph} from "../../../service/serverService";
import {tooltip, yAxisConfigurations} from "../../../util/graphs";
import {useTranslation} from "react-i18next";
import {CardLoader, ChartLoader} from "../../navigation/Loader";
import LineGraph from "../../graphs/LineGraph";
import {ErrorViewBody, ErrorViewCard} from "../../../views/ErrorView";
import PingGraph from "../../graphs/performance/PingGraph";
import {useMetadata} from "../../../hooks/metadataHook";
const Tab = ({data, yAxis}) => {
return (
<LineGraph id={'performance-' + new Date().getTime()} series={data} legendEnabled tall yAxis={yAxis}/>
)
}
const PingTab = ({identifier}) => {
const {data, loadingError} = useDataRequest(fetchPingGraph, [identifier]);
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <ChartLoader style={{height: "450px"}}/>;
return <PingGraph id="network-performance-ping-chart" data={data}/>;
}
const PerformanceGraphsCard = ({data}) => {
const {t} = useTranslation();
const {networkMetadata} = useMetadata();
if (!data || !Object.values(data).length) return <CardLoader/>
const zones = {
tps: [{
value: data.zones.tpsThresholdMed,
color: data.colors.low
}, {
value: data.zones.tpsThresholdHigh,
color: data.colors.med
}, {
value: 30,
color: data.colors.high
}],
disk: [{
value: data.zones.diskThresholdMed,
color: data.colors.low
}, {
value: data.zones.diskThresholdHigh,
color: data.colors.med
}, {
value: Number.MAX_VALUE,
color: data.colors.high
}]
};
const serverData = [];
for (let i = 0; i < data.servers.length; i++) {
const server = data.servers[i];
const values = data.values[i];
serverData.push({
serverName: server.serverName,
values
});
}
const series = {
players: [],
tps: [],
cpu: [],
ram: [],
entities: [],
chunks: [],
disk: []
}
const spline = 'spline';
for (const server of serverData) {
series.players.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.playersOnline, color: data.colors.playersOnline, yAxis: 0
});
series.tps.push({
name: server.serverName, type: spline, tooltip: tooltip.twoDecimals,
data: server.values.tps, color: data.colors.high, zones: zones.tps, yAxis: 0
});
series.cpu.push({
name: server.serverName, type: spline, tooltip: tooltip.twoDecimals,
data: server.values.cpu, color: data.colors.cpu, yAxis: 0
});
series.ram.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.ram, color: data.colors.ram, yAxis: 0
});
series.entities.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.entities, color: data.colors.entities, yAxis: 0
});
series.chunks.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.chunks, color: data.colors.chunks, yAxis: 0
});
series.disk.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.disk, color: data.colors.high, zones: zones.disk, yAxis: 0
});
}
if (data.errors.length) {
return <ErrorViewCard error={data.errors[0]}/>
}
return (
<Card>
<CardTabs tabs={[
{
name: t('html.label.playersOnline'), icon: faUser, color: 'light-blue', href: 'players-online',
element: <Tab data={series.players} yAxis={yAxisConfigurations.PLAYERS_ONLINE}/>
}, {
name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps',
element: <Tab data={series.tps} yAxis={yAxisConfigurations.TPS}/>
}, {
name: t('html.label.cpu'), icon: faTachometerAlt, color: 'amber', href: 'cpu',
element: <Tab data={series.cpu} yAxis={yAxisConfigurations.CPU}/>
}, {
name: t('html.label.ram'), icon: faMicrochip, color: 'light-green', href: 'ram',
element: <Tab data={series.ram} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
}, {
name: t('html.label.entities'), icon: faDragon, color: 'purple', href: 'entities',
element: <Tab data={series.entities} yAxis={yAxisConfigurations.ENTITIES}/>
}, {
name: t('html.label.loadedChunks'), icon: faMap, color: 'blue-grey', href: 'chunks',
element: <Tab data={series.chunks} yAxis={yAxisConfigurations.CHUNKS}/>
}, {
name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk',
element: <Tab data={series.disk} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
}, {
name: t('html.label.ping'), icon: faSignal, color: 'amber', href: 'ping',
element: networkMetadata ? <PingTab identifier={networkMetadata.currentServer.serverUUID}/> :
<ChartLoader/>
},
]}/>
</Card>
)
};
export default PerformanceGraphsCard

View File

@ -0,0 +1,47 @@
import React from 'react';
import {Card} from "react-bootstrap-v5";
import CardHeader from "../CardHeader";
import {
faBookOpen,
faChartLine,
faExclamationCircle,
faPowerOff,
faTachometerAlt,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "react-i18next";
import Datapoint from "../../Datapoint";
const QuickViewDataCard = ({server}) => {
const {t} = useTranslation()
return (
<Card>
<CardHeader icon={faBookOpen} color={'light-green'} label={server.name + ' ' + t('html.label.asNumbers')}/>
<Card.Body>
<Datapoint icon={faPowerOff} color={'light-green'} name={t('html.label.currentUptime')}
value={server.current_uptime}/>
<Datapoint name={t('html.label.lastPeak') + ' (' + server.last_peak_date + ')'}
color={'blue'} icon={faChartLine}
value={server.last_peak_players} valueLabel={t('html.unit.players')} bold/>
<Datapoint name={t('html.label.bestPeak') + ' (' + server.best_peak_date + ')'}
color={'light-green'} icon={faChartLine}
value={server.best_peak_players} valueLabel={t('html.unit.players')} bold/>
<hr/>
<p><b>{t('html.label.last7days')}</b></p>
<Datapoint icon={faUsers} color={'light-blue'} name={t('html.label.uniquePlayers')}
value={server.unique_players}/>
<Datapoint icon={faUsers} color={'light-green'} name={t('html.label.newPlayers')}
value={server.new_players}/>
<Datapoint icon={faTachometerAlt} color={'orange'} name={t('html.label.averageTps')}
value={server.avg_tps}/>
<Datapoint icon={faExclamationCircle} color={'red'} name={t('html.label.lowTpsSpikes')}
value={server.low_tps_spikes}/>
<Datapoint icon={faPowerOff} color={'red'} name={t('html.label.downtime')}
value={server.downtime}/>
</Card.Body>
</Card>
)
};
export default QuickViewDataCard

View File

@ -0,0 +1,19 @@
import React from 'react';
import CardHeader from "../CardHeader";
import {Card} from "react-bootstrap-v5";
import PlayersOnlineGraph from "../../graphs/PlayersOnlineGraph";
import {faChartArea} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "react-i18next";
const QuickViewGraphCard = ({server}) => {
const {t} = useTranslation();
return (
<Card>
<CardHeader icon={faChartArea} color={'light-blue'}
label={server.name + ' ' + t('html.label.onlineActivity') + ' (' + t('html.label.thirtyDays') + ')'}/>
<PlayersOnlineGraph data={server.playersOnline}/>
</Card>
)
};
export default QuickViewGraphCard

View File

@ -0,0 +1,73 @@
import React, {useCallback, useState} from 'react';
import {Card, Dropdown} from "react-bootstrap-v5";
import ServersTable, {ServerSortOption} from "../../table/ServersTable";
import {faNetworkWired} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "react-i18next";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import DropdownToggle from "react-bootstrap-v5/lib/esm/DropdownToggle";
import DropdownMenu from "react-bootstrap-v5/lib/esm/DropdownMenu";
import DropdownItem from "react-bootstrap-v5/lib/esm/DropdownItem";
const SortDropDown = ({sortBy, sortReversed, setSortBy}) => {
const {t} = useTranslation();
const sortOptions = Object.values(ServerSortOption);
const getSortIcon = useCallback(() => {
return sortReversed ? sortBy.iconDesc : sortBy.iconAsc;
}, [sortBy, sortReversed]);
return (
<Dropdown className="float-end" style={{position: "absolute", right: "0.5rem"}}>
<DropdownToggle variant=''>
<Fa icon={getSortIcon()}/> {t(sortBy.label)}
</DropdownToggle>
<DropdownMenu>
<h6 className="dropdown-header">{t('html.label.sortBy')}</h6>
{sortOptions.map((option, i) => (
<DropdownItem key={i} onClick={() => setSortBy(option)}>
{t(option.label)}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
)
}
const ServersTableCard = ({servers, onSelect}) => {
const {t} = useTranslation();
const [sortBy, setSortBy] = useState(ServerSortOption.ALPHABETICAL);
const [sortReversed, setSortReversed] = useState(false);
const setSort = option => {
if (sortBy === option) {
setSortReversed(!sortReversed);
} else {
setSortBy(option);
setSortReversed(false);
}
}
return (
<Card>
<Card.Header style={{width: "100%"}}>
<h6 className="col-black">
<Fa icon={faNetworkWired} className={"col-light-green"}/> {t('html.label.servers')}
</h6>
<SortDropDown sortBy={sortBy} setSortBy={setSort} sortReversed={sortReversed}/>
</Card.Header>
{!servers.length && <Card.Body>
<p>No servers found in the database.</p>
<p>It appears that Plan is not installed on any game servers or not connected to the same database.
See <a href="https://github.com/plan-player-analytics/Plan/wiki">wiki</a> for Network tutorial.</p>
</Card.Body>}
{servers.length && <ServersTable servers={servers}
onSelect={onSelect}
sortBy={sortBy}
sortReversed={sortReversed}/>}
</Card>
)
};
export default ServersTableCard

View File

@ -1,5 +1,4 @@
import React from "react";
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchPlayerbaseDevelopmentGraph} from "../../../../service/serverService";
import {ErrorViewCard} from "../../../../views/ErrorView";
@ -7,12 +6,11 @@ import {useTranslation} from "react-i18next";
import {Card} from "react-bootstrap-v5";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faUsers} from "@fortawesome/free-solid-svg-icons";
import PlayerbasePie from "../../../graphs/PlayerbasePie";
import {CardLoader} from "../../../navigation/Loader";
import GroupVisualizer from "../../../graphs/GroupVisualizer";
const CurrentPlayerbaseCard = () => {
const CurrentPlayerbaseCard = ({identifier}) => {
const {t} = useTranslation();
const {identifier} = useParams();
const {data, loadingError} = useDataRequest(fetchPlayerbaseDevelopmentGraph, [identifier]);
@ -26,7 +24,7 @@ const CurrentPlayerbaseCard = () => {
<Fa icon={faUsers} className="col-amber"/> {t('html.label.currentPlayerbase')}
</h6>
</Card.Header>
<PlayerbasePie series={data.activity_pie_series}/>
<GroupVisualizer groups={data.activity_pie_series} name={t('html.label.players')}/>
</Card>
)
}

View File

@ -0,0 +1,37 @@
import React, {useState} from 'react';
import {useTranslation} from "react-i18next";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchJoinAddressByDay} from "../../../../service/serverService";
import {ErrorViewCard} from "../../../../views/ErrorView";
import {CardLoader} from "../../../navigation/Loader";
import {Card} from "react-bootstrap-v5";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faChartColumn} from "@fortawesome/free-solid-svg-icons";
import JoinAddressGraph from "../../../graphs/JoinAddressGraph";
import Toggle from "../../../input/Toggle";
const JoinAddressGraphCard = ({identifier}) => {
const {t} = useTranslation();
const [stack, setStack] = useState(true);
const {data, loadingError} = useDataRequest(fetchJoinAddressByDay, [identifier]);
if (loadingError) return <ErrorViewCard error={loadingError}/>
if (!data) return <CardLoader/>;
return (
<Card>
<Card.Header>
<h6 className="col-black" style={{width: '100%'}}>
<Fa icon={faChartColumn} className="col-amber"/> {t('html.label.joinAddresses')}
</h6>
<Toggle value={stack} onValueChange={setStack} color={'amber'}>{t('html.label.stacked')}</Toggle>
</Card.Header>
<JoinAddressGraph id={'join-address-graph'} data={data?.join_addresses_by_date} colors={data?.colors}
stack={stack}/>
</Card>
)
};
export default JoinAddressGraphCard

View File

@ -0,0 +1,32 @@
import React from 'react';
import {useTranslation} from "react-i18next";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchJoinAddressPie} from "../../../../service/serverService";
import {ErrorViewCard} from "../../../../views/ErrorView";
import {CardLoader} from "../../../navigation/Loader";
import {Card} from "react-bootstrap-v5";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faLocationArrow} from "@fortawesome/free-solid-svg-icons";
import GroupVisualizer from "../../../graphs/GroupVisualizer";
const JoinAddressGroupCard = ({identifier}) => {
const {t} = useTranslation();
const {data, loadingError} = useDataRequest(fetchJoinAddressPie, [identifier]);
if (loadingError) return <ErrorViewCard error={loadingError}/>
if (!data) return <CardLoader/>;
return (
<Card>
<Card.Header>
<h6 className="col-black" style={{width: '100%'}}>
<Fa icon={faLocationArrow} className="col-amber"/> {t('html.label.latestJoinAddresses')}
</h6>
</Card.Header>
<GroupVisualizer groups={data.slices} colors={data.colors}/>
</Card>
)
};
export default JoinAddressGroupCard

View File

@ -14,6 +14,7 @@ import CpuRamPerformanceGraph from "../../../graphs/performance/CpuRamPerformanc
import WorldPerformanceGraph from "../../../graphs/performance/WorldPerformanceGraph";
import DiskPerformanceGraph from "../../../graphs/performance/DiskPerformanceGraph";
import PingGraph from "../../../graphs/performance/PingGraph";
import {mapPerformanceDataToSeries} from "../../../../util/graphs";
const AllGraphTab = ({data, dataSeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
@ -58,43 +59,6 @@ const PingGraphTab = ({identifier}) => {
return <PingGraph id="server-performance-ping-chart" data={data}/>;
}
function mapToDataSeries(performanceData) {
const playersOnline = [];
const tps = [];
const cpu = [];
const ram = [];
const entities = [];
const chunks = [];
const disk = [];
return new Promise((resolve => {
let i = 0;
const length = performanceData.length;
function processNextThousand() {
const to = Math.min(i + 1000, length);
for (i; i < to; i++) {
const entry = performanceData[i];
const date = entry[0];
playersOnline[i] = [date, entry[1]];
tps[i] = [date, entry[2]];
cpu[i] = [date, entry[3]];
ram[i] = [date, entry[4]];
entities[i] = [date, entry[5]];
chunks[i] = [date, entry[6]];
disk[i] = [date, entry[7]];
}
if (i >= length) {
resolve({playersOnline, tps, cpu, ram, entities, chunks, disk})
} else {
setTimeout(processNextThousand, 10);
}
}
processNextThousand();
}))
}
const PerformanceGraphsCard = () => {
const {t} = useTranslation();
@ -104,7 +68,7 @@ const PerformanceGraphsCard = () => {
useEffect(() => {
if (data) {
mapToDataSeries(data.values).then(parsed => setParsedData(parsed))
mapPerformanceDataToSeries(data.values).then(parsed => setParsedData(parsed))
}
}, [data, setParsedData]);

View File

@ -1,5 +1,4 @@
import {useTranslation} from "react-i18next";
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchPlayerbaseDevelopmentGraph} from "../../../../service/serverService";
import {ErrorViewCard} from "../../../../views/ErrorView";
@ -10,9 +9,8 @@ import React from "react";
import PlayerbaseGraph from "../../../graphs/PlayerbaseGraph";
import {CardLoader} from "../../../navigation/Loader";
const PlayerbaseDevelopmentCard = () => {
const PlayerbaseDevelopmentCard = ({identifier}) => {
const {t} = useTranslation();
const {identifier} = useParams();
const {data, loadingError} = useDataRequest(
fetchPlayerbaseDevelopmentGraph,

View File

@ -1,6 +1,5 @@
import React from "react";
import InsightsFor30DaysCard from "../../common/InsightsFor30DaysCard";
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchSessionOverview} from "../../../../service/serverService";
import {ErrorViewCard} from "../../../../views/ErrorView";
@ -8,29 +7,35 @@ import Datapoint from "../../../Datapoint";
import {useTranslation} from "react-i18next";
import {faGamepad, faUsers} from "@fortawesome/free-solid-svg-icons";
import {faClock} from "@fortawesome/free-regular-svg-icons";
import {fetchNetworkSessionsOverview} from "../../../../service/networkService";
const SessionInsightsCard = () => {
const SessionInsightsCard = ({identifier}) => {
const {t} = useTranslation();
const {identifier} = useParams();
const {data, loadingError} = useDataRequest(fetchSessionOverview, [identifier]);
const {
data,
loadingError
} = useDataRequest(identifier ? fetchSessionOverview : fetchNetworkSessionsOverview, [identifier]);
if (loadingError) return <ErrorViewCard error={loadingError}/>
const insights = data?.insights;
return (
<InsightsFor30DaysCard>
<Datapoint name={t('html.label.mostActiveGamemode')} icon={faGamepad} color="teal" bold
value={data?.insights.most_active_gamemode}
valueLabel={data?.insights.most_active_gamemode_perc}
value={insights?.most_active_gamemode}
valueLabel={insights?.most_active_gamemode_perc}
/>
<Datapoint name={t('html.label.serverOccupied')} icon={faUsers} color="teal"
value={'~' + data?.insights.server_occupied} valueLabel={data?.insights.server_occupied_perc}
value={insights?.server_occupied ? '~' + insights.server_occupied : undefined}
valueLabel={insights?.server_occupied_perc}
/>
<Datapoint name={t('html.label.playtime')} icon={faClock} color="green"
value={data?.insights.total_playtime}
value={insights?.total_playtime}
/>
<Datapoint name={t('html.label.afkTime')} icon={faClock} color="grey"
value={data?.insights.afk_time} valueLabel={data?.insights.afk_time_perc}
value={insights?.afk_time} valueLabel={insights?.afk_time_perc}
/>
</InsightsFor30DaysCard>
)

View File

@ -1,20 +1,16 @@
import React from "react";
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchSessions} from "../../../../service/serverService";
import {ErrorViewCard} from "../../../../views/ErrorView";
import RecentSessionsCard from "../../common/RecentSessionsCard";
const ServerRecentSessionsCard = () => {
const {identifier} = useParams();
const ServerRecentSessionsCard = ({identifier}) => {
const {data, loadingError} = useDataRequest(fetchSessions, [identifier])
if (loadingError) return <ErrorViewCard error={loadingError}/>
return (
<RecentSessionsCard sessions={data?.sessions} isPlayer={true}/>
<RecentSessionsCard sessions={data?.sessions} isPlayer={true} isNetwork={!identifier}/>
)
}

View File

@ -13,8 +13,8 @@ const ExtensionIcon = ({icon}) => {
)
}
export const toExtensionIconHtmlString = ({icon}) => {
return icon ? `<i class="${iconTypeToFontAwesomeClass(icon.family)} ${icon.iconName} ${icon.colorClass}"></i>` : '';
export const toExtensionIconHtmlString = (icon) => {
return icon ? `<i class="${iconTypeToFontAwesomeClass(icon.family)} fa-${icon.iconName} ${icon.colorClass}"></i>` : '';
}
export default ExtensionIcon;

View File

@ -1,12 +1,34 @@
import React, {useEffect} from 'react';
import {useTranslation} from "react-i18next";
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import Highcharts from 'highcharts/highmaps.js';
import map from '@highcharts/map-collection/custom/world.geo.json';
import topology from '@highcharts/map-collection/custom/world.topo.json';
import Accessibility from "highcharts/modules/accessibility";
import NoDataDisplay from "highcharts/modules/no-data-to-display";
const GeolocationWorldMap = ({series, colors}) => {
export const ProjectionOptions = {
MILLER: "html.label.geoProjection.miller",
MERCATOR: "html.label.geoProjection.mercator",
EQUAL_EARTH: "html.label.geoProjection.equalEarth"
// ORTOGRAPHIC: "html.label.geoProjection.ortographic"
}
const getProjection = option => {
switch (option) {
case ProjectionOptions.MERCATOR:
return {name: 'WebMercator'};
case ProjectionOptions.EQUAL_EARTH:
return {name: 'EqualEarth'};
// Ortographic projection stops working after a while for some reason
// case ProjectionOptions.ORTOGRAPHIC:
// return {name: 'Orthographic'};
case ProjectionOptions.MILLER:
default:
return {name: 'Miller'};
}
}
const GeolocationWorldMap = ({series, colors, projection}) => {
const {t} = useTranslation();
const {nightModeEnabled, graphTheming} = useTheme();
@ -14,33 +36,41 @@ const GeolocationWorldMap = ({series, colors}) => {
const mapSeries = {
name: t('html.label.players'),
type: 'map',
mapData: map,
data: series,
joinBy: ['iso-a3', 'code']
};
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions(graphTheming);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}});
Highcharts.mapChart('countryWorldMap', {
chart: {
map: topology,
animation: true
},
title: {text: ''},
mapNavigation: {
enabled: true,
enableDoubleClickZoomTo: true
enableDoubleClickZoomTo: true,
enableMouseWheelZoom: true,
enableTouchZoom: true
},
mapView: {
projection: getProjection(projection)
},
colorAxis: {
min: 1,
type: 'logarithmic',
minColor: nightModeEnabled ? withReducedSaturation(colors.low) : colors.low,
maxColor: nightModeEnabled ? withReducedSaturation(colors.high) : colors.high
minColor: colors.low,
maxColor: colors.high
},
series: [mapSeries]
})
}, [colors, series, graphTheming, nightModeEnabled, t]);
}, [colors, series, graphTheming, nightModeEnabled, t, projection]);
return (<div id="countryWorldMap"/>);
};

View File

@ -0,0 +1,56 @@
import React, {useEffect} from 'react';
import {useTranslation} from "react-i18next";
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import Highcharts from "highcharts";
import Accessibility from "highcharts/modules/accessibility";
const GroupBarGraph = ({id, groups, colors, horizontal, name}) => {
const {t} = useTranslation();
const {nightModeEnabled, graphTheming} = useTheme();
useEffect(() => {
const reduceColors = (colorsToReduce) => colorsToReduce.map(color => withReducedSaturation(color));
function getColors() {
const actualColors = colors ? colors : groups.map(group => group.color);
return nightModeEnabled ? reduceColors(actualColors) : actualColors;
}
const bars = groups.map(group => group.y);
const categories = groups.map(group => t(group.name));
const barSeries = {
name: name,
colorByPoint: true,
data: bars,
colors: getColors()
};
Accessibility(Highcharts);
Highcharts.setOptions(graphTheming);
Highcharts.chart(id, {
chart: {type: horizontal ? 'bar' : 'column'},
title: {text: ''},
xAxis: {
categories: categories,
title: {text: ''}
},
yAxis: {
min: 0,
title: {text: '', align: 'high'},
labels: {overflow: 'justify'}
},
legend: {enabled: false},
plotOptions: {
bar: {
dataLabels: {enabled: true}
}
},
series: [barSeries]
})
}, [id, groups, colors, horizontal, name, graphTheming, nightModeEnabled, t]);
return (<div id={id} className="chart-area"/>);
};
export default GroupBarGraph

View File

@ -1,28 +1,35 @@
import React, {useEffect} from "react";
import Highcharts from 'highcharts';
import React, {useEffect} from 'react';
import {useTranslation} from "react-i18next";
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import {useTranslation} from "react-i18next";
import Accessibility from "highcharts/modules/accessibility";
import Highcharts from "highcharts";
const PlayerbasePie = ({series}) => {
const GroupPie = ({id, groups, colors, name}) => {
const {t} = useTranslation();
const {nightModeEnabled, graphTheming} = useTheme();
useEffect(() => {
const reduceColors = (slices) => slices.map(slice => {
return {...slice, color: withReducedSaturation(slice.color)}
});
const reduceColors = (colorsToReduce) => colorsToReduce.map(color => withReducedSaturation(color));
function getColors() {
const actualColors = colors ? colors : groups.map(group => group.color);
return nightModeEnabled ? reduceColors(actualColors) : actualColors;
}
const series = groups.map(group => {
return {name: t(group.name), y: group.y}
});
const pieSeries = {
name: t('html.label.players'),
name: name,
colorByPoint: true,
data: nightModeEnabled ? reduceColors(series) : series
colors: getColors(),
data: series
};
Accessibility(Highcharts);
Highcharts.setOptions(graphTheming);
Highcharts.chart('playerbase-pie', {
Highcharts.chart(id, {
chart: {
backgroundColor: 'transparent',
plotBorderWidth: null,
@ -40,11 +47,16 @@ const PlayerbasePie = ({series}) => {
showInLegend: true
}
},
tooltip: {
formatter: function () {
return '<b>' + this.point.name + ':</b> ' + this.y;
}
},
series: [pieSeries]
});
}, [series, graphTheming, nightModeEnabled, t]);
}, [id, colors, groups, name, graphTheming, nightModeEnabled, t]);
return (<div className="chart-area" id="playerbase-pie"/>);
}
return (<div className="chart-area" id={id}/>);
};
export default PlayerbasePie;
export default GroupPie;

View File

@ -0,0 +1,63 @@
import React, {useState} from 'react';
import GroupTable from "../table/GroupTable";
import GroupPie from "./GroupPie";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faBarChart, faChartColumn, faPieChart, faTable} from "@fortawesome/free-solid-svg-icons";
import {Col, Row} from "react-bootstrap-v5";
import GroupBarGraph from "./GroupBarGraph";
const options = {
BAR: 'bar',
COLUMN: 'column',
PIE: 'pie',
TABLE: 'table'
}
const Visualizer = ({option, groups, colors, name}) => {
switch (option) {
case options.TABLE:
return <GroupTable groups={groups} colors={colors}/>
case options.PIE:
return <GroupPie id={'group-pie-' + new Date()} groups={groups} colors={colors} name={name}/>
case options.BAR:
return <GroupBarGraph id={'group-bar-' + new Date()} groups={groups} colors={colors} name={name}
horizontal/>;
case options.COLUMN:
default:
return <GroupBarGraph id={'group-bar-' + new Date()} groups={groups} colors={colors} name={name}/>;
}
}
const VisualizerSelector = ({onClick, icon}) => {
return (
<button className="btn float-end" onClick={onClick}>
<FontAwesomeIcon icon={icon} className="col-gray"/>
</button>
)
}
const GroupVisualizer = ({groups, colors, name, horizontal}) => {
const [visualization, setVisualization] = useState(groups.length > 1 ? options.COLUMN : options.TABLE);
const selectorFloatStyle = {
height: "0",
zIndex: 100,
position: "absolute",
width: "100%",
right: "0",
top: "0.5rem"
};
return <Row>
<Col md={12} style={selectorFloatStyle}>
<VisualizerSelector icon={faPieChart} onClick={() => setVisualization(options.PIE)}/>
<VisualizerSelector icon={faTable} onClick={() => setVisualization(options.TABLE)}/>
<VisualizerSelector icon={horizontal ? faBarChart : faChartColumn}
onClick={() => setVisualization(horizontal ? options.BAR : options.COLUMN)}/>
</Col>
<Col md={12}>
<Visualizer option={visualization} groups={groups} colors={colors} name={name}/>
</Col>
</Row>
};
export default GroupVisualizer

View File

@ -0,0 +1,82 @@
import React, {useEffect} from 'react';
import {useTranslation} from "react-i18next";
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import NoDataDisplay from "highcharts/modules/no-data-to-display";
import Highcharts from "highcharts/highstock";
import Accessibility from "highcharts/modules/accessibility";
import {linegraphButtons} from "../../util/graphs";
const JoinAddressGraph = ({id, data, colors, stack}) => {
const {t} = useTranslation()
const {nightModeEnabled, graphTheming} = useTheme();
useEffect(() => {
const getColor = i => {
const color = colors[i % colors.length];
return nightModeEnabled ? withReducedSaturation(color) : color;
}
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
Highcharts.setOptions(graphTheming);
const valuesByAddress = {};
const dates = []
for (const point of data || []) {
dates.push(point.date);
for (const address of point.joinAddresses) {
if (!valuesByAddress[address.joinAddress]) valuesByAddress[address.joinAddress] = [];
valuesByAddress[address.joinAddress].push([point.date, address.count]);
}
}
const labels = dates;
const series = Object.entries(valuesByAddress).map((entry, i) => {
if (i >= colors.length) return {name: entry[0], data: entry[1]};
return {name: entry[0], data: entry[1], color: getColor(i)};
});
Highcharts.stockChart(id, {
chart: {
type: "column"
},
rangeSelector: {
selected: 3,
buttons: linegraphButtons
},
xAxis: {
categories: labels,
tickmarkPlacement: 'on',
title: {
enabled: false
},
ordinal: false
},
yAxis: {
softMax: 2,
softMin: 0
},
title: {text: ''},
plotOptions: {
column: {
stacking: stack ? 'normal' : undefined,
lineWidth: 1
}
},
legend: {
enabled: true
},
series: series
})
}, [data, colors, graphTheming, id, t, nightModeEnabled, stack])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>
<span className="loader"/>
</div>
)
};
export default JoinAddressGraph

View File

@ -6,7 +6,7 @@ import NoDataDisplay from "highcharts/modules/no-data-to-display"
import Accessibility from "highcharts/modules/accessibility"
import {useTranslation} from "react-i18next";
const LineGraph = ({id, series}) => {
const LineGraph = ({id, series, legendEnabled, tall, yAxis}) => {
const {t} = useTranslation()
const {graphTheming, nightModeEnabled} = useTheme();
@ -20,7 +20,7 @@ const LineGraph = ({id, series}) => {
selected: 2,
buttons: linegraphButtons
},
yAxis: {
yAxis: yAxis || {
softMax: 2,
softMin: 0
},
@ -30,12 +30,17 @@ const LineGraph = ({id, series}) => {
fillOpacity: nightModeEnabled ? 0.2 : 0.4
}
},
legend: {
enabled: legendEnabled
},
series: series
})
}, [series, graphTheming, id, t, nightModeEnabled])
}, [series, graphTheming, id, t, nightModeEnabled, legendEnabled, yAxis])
const style = tall ? {height: "450px"} : undefined;
return (
<div className="chart-area" id={id}>
<div className="chart-area" style={style} id={id}>
<span className="loader"/>
</div>
)

View File

@ -5,6 +5,7 @@ import {formatTimeAmount} from '../../util/formatters'
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import {useTranslation} from "react-i18next";
import NoDataDisplay from "highcharts/modules/no-data-to-display";
import Accessibility from "highcharts/modules/accessibility";
const ServerPie = ({colors, series}) => {
@ -21,8 +22,10 @@ const ServerPie = ({colors, series}) => {
data: series
};
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions(graphTheming);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}});
Highcharts.chart('server-pie', {
chart: {
backgroundColor: 'transparent',

View File

@ -0,0 +1,29 @@
import React from 'react';
import DropdownToggle from "react-bootstrap-v5/lib/esm/DropdownToggle";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import DropdownMenu from "react-bootstrap-v5/lib/esm/DropdownMenu";
import DropdownItem from "react-bootstrap-v5/lib/esm/DropdownItem";
import {useTranslation} from "react-i18next";
const BasicDropdown = ({selected, optionList, onChange, optionLabelMapper, icon, title}) => {
const {t} = useTranslation();
return (
<BasicDropdown className="float-end" style={{position: "absolute", right: "0.5rem"}} title={t(title)}>
<DropdownToggle variant=''>
<Fa icon={icon}/> {t(optionLabelMapper ? optionLabelMapper(selected) : selected)}
</DropdownToggle>
<DropdownMenu>
<h6 className="dropdown-header">{t(title)}</h6>
{optionList.map((option, i) => (
<DropdownItem key={i} onClick={() => onChange(option)}>
{t(optionLabelMapper ? optionLabelMapper(option) : option)}
</DropdownItem>
))}
</DropdownMenu>
</BasicDropdown>
)
};
export default BasicDropdown

View File

@ -0,0 +1,24 @@
import React from 'react';
const MultiSelect = ({options, selectedIndexes, setSelectedIndexes}) => {
const handleChange = (event) => {
const renderedOptions = Object.values(event.target.selectedOptions)
.map(htmlElement => htmlElement.text)
.map(option => options.indexOf(option));
setSelectedIndexes(renderedOptions);
}
return (
<select className="form-control" multiple
onChange={handleChange}>
{options.map((option, i) => {
return (
<option key={i} value={selectedIndexes.includes(i)}
selected={selectedIndexes.includes(i)}>{option}</option>
)
})}
</select>
)
};
export default MultiSelect

View File

@ -0,0 +1,20 @@
import React, {useState} from 'react';
const Toggle = ({children, value, onValueChange, color}) => {
const [renderTime] = useState(new Date().getTime());
const id = 'checkbox-' + renderTime;
const handleChange = () => {
onValueChange(!value);
}
return (
<div className="form-check form-switch">
<input id={id} type={"checkbox"} className={"form-check-input bg-" + color} role="switch"
onChange={handleChange} checked={value}/>
<label className="form-check-label" htmlFor={id}>{children}</label>
</div>
)
};
export default Toggle

View File

@ -36,7 +36,7 @@ const Header = ({page, tab}) => {
const {toggleColorChooser} = useTheme();
const {t} = useTranslation();
const {requestUpdate, updating, lastUpdate, toggleSidebar} = useNavigation();
const {requestUpdate, lastUpdate, updating, toggleSidebar} = useNavigation();
const {getPlayerHeadImageUrl} = useMetadata();
const headImageUrl = user ? getPlayerHeadImageUrl(user.playerName, user.linkedToUuid) : undefined
@ -59,7 +59,7 @@ const Header = ({page, tab}) => {
<span className="topbar-divider"/>
<div className="refresh-element">
<button onClick={requestUpdate}>
<Fa icon={faSyncAlt} spin={updating}/>
<Fa icon={faSyncAlt} spin={Boolean(updating)}/>
</button>
{' '}
<span className="refresh-time">{lastUpdate.formatted}</span>

View File

@ -0,0 +1,47 @@
import React from 'react';
import {useTranslation} from "react-i18next";
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import Scrollable from "../Scrollable";
const GroupRow = ({group, color}) => {
return (
<tr>
<td style={{color}}>{group.name}</td>
<td>{group.y}</td>
</tr>
)
}
const GroupTable = ({groups, colors}) => {
const {t} = useTranslation();
const {nightModeEnabled} = useTheme();
function getColor(i) {
if (groups[i].color) {
return nightModeEnabled ? withReducedSaturation(groups[i].color) : groups[i].color;
}
return nightModeEnabled ? withReducedSaturation(colors[i]) : colors[i];
}
return (
<Scrollable>
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
<tbody>
{groups.length ? groups.map((group, i) =>
<GroupRow key={i}
group={group}
color={getColor(i)}/>) :
<tr>
<td>{t('generic.noData')}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>}
</tbody>
</table>
</Scrollable>
)
};
export default GroupTable

View File

@ -14,11 +14,11 @@ import {TableRow} from "./TableRow";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faEye} from "@fortawesome/free-regular-svg-icons";
import AsNumbersTable from "./AsNumbersTable";
import {CardLoader} from "../navigation/Loader";
import {ChartLoader} from "../navigation/Loader";
const PerformanceAsNumbersTable = ({data}) => {
const {t} = useTranslation();
if (!data) return <CardLoader/>;
if (!data) return <ChartLoader/>;
return (
<AsNumbersTable
@ -31,12 +31,19 @@ const PerformanceAsNumbersTable = ({data}) => {
data.low_tps_spikes_24h
]}/>
<TableRow icon={faPowerOff} color="red"
text={t('html.label.serverDowntime') + ' (' + t('generic.noData') + ')'}
text={t(data.avg_server_downtime_30d ? 'html.label.serverDowntime' : 'html.label.totalServerDowntime') + ' (' + t('generic.noData') + ')'}
values={[
data.server_downtime_30d,
data.server_downtime_7d,
data.server_downtime_24h
]}/>
<TableRow icon={faPowerOff} color="red"
text={t('html.label.averageServerDowntime')}
values={[
data.avg_server_downtime_30d,
data.avg_server_downtime_7d,
data.avg_server_downtime_24h
]}/>
<TableRow icon={faUser} color="light-blue" text={t('html.label.averagePlayers')}
values={[
data.players_30d,

View File

@ -1,6 +1,7 @@
import React from "react";
import {useTheme} from "../../hooks/themeHook";
import {useTranslation} from "react-i18next";
import Scrollable from "../Scrollable";
const PingRow = ({country}) => {
return (
@ -18,24 +19,26 @@ const PingTable = ({countries}) => {
const {nightModeEnabled} = useTheme();
return (
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
<thead className="bg-amber">
<tr>
<th>{t('html.label.country')}</th>
<th>{t('html.label.averagePing')}</th>
<th>{t('html.label.bestPing')}</th>
<th>{t('html.label.worstPing')}</th>
</tr>
</thead>
<tbody>
{countries.length ? countries.map((country, i) => <PingRow key={i} country={country}/>) : <tr>
<td>{t('generic.noData')}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>}
</tbody>
</table>
<Scrollable>
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
<thead className="bg-amber" style={{position: "sticky", top: 0}}>
<tr>
<th>{t('html.label.country')}</th>
<th>{t('html.label.averagePing')}</th>
<th>{t('html.label.bestPing')}</th>
<th>{t('html.label.worstPing')}</th>
</tr>
</thead>
<tbody>
{countries.length ? countries.map((country, i) => <PingRow key={i} country={country}/>) : <tr>
<td>{t('generic.noData')}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>}
</tbody>
</table>
</Scrollable>
)
};

View File

@ -0,0 +1,153 @@
import React from "react";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {
faCaretSquareRight,
faLineChart,
faLink,
faServer,
faSortAlphaDown,
faSortAlphaUp,
faSortNumericDown,
faSortNumericUp,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import {useTheme} from "../../hooks/themeHook";
import {useTranslation} from "react-i18next";
import Scrollable from "../Scrollable";
import {NavLink} from "react-router-dom";
const ServerRow = ({server, onQuickView}) => {
const {t} = useTranslation();
return (
<tr>
<td>{server.name}</td>
<td className="p-1">
<NavLink to={"/server/" + encodeURIComponent(server.serverUUID)}
title={t('html.label.serverAnalysis') + ': ' + server.name}
className={'btn bg-transparent col-light-green'}><Fa
icon={faLink}/> {t('html.label.serverAnalysis')}
</NavLink>
</td>
<td>{server.players}</td>
<td>{server.online}</td>
<td className="p-1">
<button className={'btn bg-light-blue float-right'}
title={t('html.label.quickView') + ': ' + server.name}
onClick={onQuickView}
>
<Fa icon={faCaretSquareRight}/>
</button>
</td>
</tr>
);
}
const sortKeepOrder = () => 0;
const sortBySometimesNumericProperty = (propertyName) => (a, b) => {
if (typeof (a[propertyName]) === 'number' && typeof (b[propertyName]) === 'number') return a[propertyName] - b[propertyName];
if (typeof (a[propertyName]) === 'number') return 1;
if (typeof (b[propertyName]) === 'number') return -1;
return 0;
}
const sortByNumericProperty = (propertyName) => (a, b) => b[propertyName] - a[propertyName]; // Biggest first
const sortBeforeReverse = (servers, sortBy) => {
return [...servers].sort(sortBy.sortFunction);
}
const reverse = (array) => {
const reversedArray = [];
for (let i = array.length - 1; i >= 0; i--) {
reversedArray.push(array[i]);
}
return reversedArray;
}
const sort = (servers, sortBy, sortReversed) => {
return sortReversed ? reverse(sortBeforeReverse(servers, sortBy)) : sortBeforeReverse(servers, sortBy);
}
const SortOptionIcon = {
LETTERS: {
iconAsc: faSortAlphaDown,
iconDesc: faSortAlphaUp
},
NUMBERS: {
iconAsc: faSortNumericUp,
iconDesc: faSortNumericDown
}
}
export const ServerSortOption = {
ALPHABETICAL: {
label: 'html.label.alphabetical',
sortFunction: sortKeepOrder,
...SortOptionIcon.LETTERS
},
AVERAGE_TPS: {
label: 'html.label.averageTps7days',
sortFunction: sortBySometimesNumericProperty('avg_tps'),
...SortOptionIcon.NUMBERS
},
// DOWNTIME: 'html.label.downtime',
LOW_TPS_SPIKES: {
label: 'html.label.lowTpsSpikes7days',
sortFunction: sortByNumericProperty('low_tps_spikes'),
...SortOptionIcon.NUMBERS
},
NEW_PLAYERS: {
label: 'html.label.newPlayers7days',
sortFunction: sortByNumericProperty('new_players'),
...SortOptionIcon.NUMBERS
},
PLAYERS_ONLINE: {
label: 'html.label.playersOnlineNow',
sortFunction: sortBySometimesNumericProperty('online'),
...SortOptionIcon.NUMBERS
},
REGISTERED_PLAYERS: {
label: 'html.label.registeredPlayers',
sortFunction: sortByNumericProperty('players'),
...SortOptionIcon.NUMBERS
},
UNIQUE_PLAYERS: {
label: 'html.label.uniquePlayers7days',
sortFunction: sortByNumericProperty('unique_players'),
...SortOptionIcon.NUMBERS
},
}
const ServersTable = ({servers, onSelect, sortBy, sortReversed}) => {
const {t} = useTranslation();
const {nightModeEnabled} = useTheme();
const sortedServers = sort(servers, sortBy, sortReversed);
return (
<Scrollable>
<table className={"table mb-0 table-striped" + (nightModeEnabled ? " table-dark" : '')}>
<thead>
<tr>
<th><Fa icon={faServer}/> {t('html.label.server')}</th>
<th><Fa icon={faLineChart}/> {t('html.label.serverAnalysis')}</th>
<th><Fa icon={faUsers}/> {t('html.label.registeredPlayers')}</th>
<th><Fa icon={faUser}/> {t('html.label.playersOnline')}</th>
<th></th>
</tr>
</thead>
<tbody>
{sortedServers.length ? sortedServers.map((server, i) => <ServerRow key={i} server={server}
onQuickView={() => onSelect(servers.indexOf(server))}/>) :
<tr>
<td>{t('html.generic.none')}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>}
</tbody>
</table>
</Scrollable>
)
};
export default ServersTable;

View File

@ -1,23 +1,64 @@
import {useEffect, useState} from "react";
import {useNavigation} from "./navigationHook";
import {useDataStore} from "./datastoreHook";
import {useMetadata} from "./metadataHook";
export const useDataRequest = (fetchMethod, parameters) => {
const [data, setData] = useState(undefined);
const [loadingError, setLoadingError] = useState(undefined);
const {updateRequested, finishUpdate} = useNavigation();
const {refreshBarrierMs} = useMetadata();
const datastore = useDataStore();
/*eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
fetchMethod(updateRequested, ...parameters).then(({data: json, error}) => {
datastore.setAsUpdating(fetchMethod);
const handleResponse = (json, error, skipOldData, timeout) => {
if (json) {
setData(json);
finishUpdate(json.timestamp, json.timestamp_f);
const timestamp = json.timestamp;
if (timestamp) {
// Data has timestamp, the data may come from cache
const acceptedTimestamp = timestamp + (refreshBarrierMs ? refreshBarrierMs : 15000);
if (acceptedTimestamp < updateRequested) {
// Request again, received data was too old
setTimeout(() => {
fetchMethod(new Date().getTime(), ...parameters)
.then(({data: json, error}) => {
handleResponse(json, error, true, timeout >= 12000 ? timeout : timeout * 2);
});
}, timeout);
} else {
// Received data was new enough to be shown
setData(json);
datastore.storeData(fetchMethod, json);
datastore.finishUpdate(fetchMethod)
finishUpdate(json.timestamp, json.timestamp_f, datastore.isSomethingUpdating());
}
if (!skipOldData) {
// Old data is shown on first pass, further passes skip old data.
setData(json);
datastore.storeData(fetchMethod, json);
finishUpdate(json.timestamp, json.timestamp_f, datastore.isSomethingUpdating());
}
} else {
// Response data is not cached, no timestamp, show it immediately
setData(json);
datastore.finishUpdate(fetchMethod);
finishUpdate(json.timestamp, json.timestamp_f, datastore.isSomethingUpdating());
}
} else if (error) {
console.warn(error);
datastore.finishUpdate(fetchMethod)
setLoadingError(error);
finishUpdate(0, "Error: " + error.message, datastore.isSomethingUpdating());
}
};
fetchMethod(updateRequested, ...parameters).then(({data: json, error}) => {
handleResponse(json, error, false, 1000);
});
}, [fetchMethod, ...parameters, updateRequested])
}, [fetchMethod, ...parameters, updateRequested, refreshBarrierMs])
/* eslint-enable react-hooks/exhaustive-deps */
return {data, loadingError};

View File

@ -0,0 +1,45 @@
import {useCallback} from "react";
import {useMetadata} from "./metadataHook";
export const useDataStore = () => {
const {datastore} = useMetadata();
if (datastore && !datastore.dataByMethod) datastore.dataByMethod = {};
if (datastore && !datastore.lastUpdateByMethod) datastore.lastUpdateByMethod = {};
if (datastore && !datastore.currentlyUpdatingMethods) datastore.currentlyUpdatingMethods = {};
const storeData = useCallback((method, data) => {
const hadPrevious = Boolean(datastore.dataByMethod[method]);
if (data) {
datastore.lastUpdateByMethod[method] = data.timestamp;
datastore.dataByMethod[method] = data;
}
return hadPrevious;
}, [datastore]);
const getLastUpdate = useCallback((method) => {
return datastore?.lastUpdateByMethod[method];
}, [datastore]);
const getData = useCallback((method) => {
return datastore?.dataByMethod[method];
}, [datastore]);
const isCurrentlyUpdating = useCallback((method) => {
return datastore && Boolean(datastore.currentlyUpdatingMethods[method]);
}, [datastore]);
const setAsUpdating = useCallback((method) => {
datastore.currentlyUpdatingMethods[method] = true;
}, [datastore]);
const finishUpdate = useCallback((method) => {
delete datastore.currentlyUpdatingMethods[method];
}, [datastore])
const isSomethingUpdating = useCallback(() => {
return datastore && Boolean(Object.values(datastore.currentlyUpdatingMethods).filter(value => Boolean(value)).length);
}, [datastore]);
return {storeData, getLastUpdate, getData, isCurrentlyUpdating, isSomethingUpdating, setAsUpdating, finishUpdate};
}

View File

@ -1,17 +1,24 @@
import {createContext, useCallback, useContext, useEffect, useState} from "react";
import {fetchPlanMetadata} from "../service/metadataService";
import {fetchNetworkMetadata, fetchPlanMetadata} from "../service/metadataService";
import terminal from '../Terminal-icon.png'
const MetadataContext = createContext({});
export const MetadataContextProvider = ({children}) => {
const [datastore] = useState({});
const [metadata, setMetadata] = useState({});
const updateMetadata = useCallback(async () => {
const {data, error} = await fetchPlanMetadata();
if (data) {
setMetadata(data);
if (data.isProxy) {
const {data: networkMetadata} = await fetchNetworkMetadata(); // error ignored
if (networkMetadata) {
setMetadata({...data, networkMetadata})
}
}
} else if (error) {
setMetadata({metadataError: error})
}
@ -34,7 +41,7 @@ export const MetadataContextProvider = ({children}) => {
updateMetadata();
}, [updateMetadata]);
const sharedState = {...metadata, getPlayerHeadImageUrl}
const sharedState = {...metadata, getPlayerHeadImageUrl, datastore}
return (<MetadataContext.Provider value={sharedState}>
{children}
</MetadataContext.Provider>

View File

@ -5,8 +5,8 @@ const NavigationContext = createContext({});
export const NavigationContextProvider = ({children}) => {
const [currentTab, setCurrentTab] = useState(undefined);
const [updateRequested, setUpdateRequested] = useState(Date.now());
const [updating, setUpdating] = useState(false);
const [lastUpdate, setLastUpdate] = useState({date: 0, formatted: ""});
const [updating, setUpdating] = useState({});
const [lastUpdate, setLastUpdate] = useState({});
const [items, setItems] = useState([]);
const [sidebarExpanded, setSidebarExpanded] = useState(window.innerWidth > 1350);
@ -31,13 +31,17 @@ export const NavigationContextProvider = ({children}) => {
}
}, [updating, setUpdateRequested, setUpdating]);
const finishUpdate = useCallback((date, formatted) => {
// TODO Logic to retry if received data is too old
// TODO currently not possible due to extensionData getting updated off-tab
// useEffect(requestUpdate, [currentTab]); // Force data to update when changing tab
const finishUpdate = useCallback((date, formatted, isStillUpdating) => {
if (date) {
setLastUpdate({date, formatted});
setUpdating(false);
if (!lastUpdate.date || date > lastUpdate.date) {
setLastUpdate({date, formatted});
}
setUpdating(isStillUpdating);
}
}, [setLastUpdate, setUpdating]);
}, [setLastUpdate, setUpdating, lastUpdate]);
const toggleSidebar = useCallback(() => {
setSidebarExpanded(!sidebarExpanded);

View File

@ -3,4 +3,34 @@ import {doGetRequest} from "./backendConfiguration";
export const fetchNetworkOverview = async (updateRequested) => {
const url = `/v1/network/overview?timestamp=${updateRequested}`;
return doGetRequest(url);
}
export const fetchServersOverview = async (updateRequested) => {
const url = `/v1/network/servers?timestamp=${updateRequested}`;
return doGetRequest(url);
}
export const fetchServerPie = async (timestamp) => {
const url = `/v1/graph?type=serverPie&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchNetworkSessionsOverview = async (timestamp) => {
const url = `/v1/network/sessionsOverview?timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchNetworkPlayerbaseOverview = async (timestamp) => {
const url = `/v1/network/playerbaseOverview?timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchNetworkPingTable = async (timestamp) => {
const url = `/v1/network/pingTable?timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchNetworkPerformanceOverview = async (timestamp, serverUUIDs) => {
const url = `/v1/network/performanceOverview?servers=${encodeURIComponent(JSON.stringify(serverUUIDs))}&timestamp=${timestamp}`;
return doGetRequest(url);
}

View File

@ -42,7 +42,8 @@ export const fetchExtensionData = async (timestamp, identifier) => {
}
export const fetchSessions = async (timestamp, identifier) => {
const url = `/v1/sessions?server=${identifier}&timestamp=${timestamp}`;
const url = identifier ? `/v1/sessions?server=${identifier}&timestamp=${timestamp}` :
`/v1/sessions?timestamp=${timestamp}`;
return doGetRequest(url);
}
@ -57,7 +58,7 @@ export const fetchPlayers = async (timestamp, identifier) => {
}
export const fetchPingTable = async (timestamp, identifier) => {
const url = identifier ? `/v1/pingTable?server=${identifier}&timestamp=${timestamp}` : `/v1/pingTable?timestamp=${timestamp}`;
const url = `/v1/pingTable?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
@ -101,12 +102,13 @@ export const fetchWorldPie = async (timestamp, identifier) => {
}
export const fetchGeolocations = async (timestamp, identifier) => {
const url = `/v1/graph?type=geolocation&server=${identifier}&timestamp=${timestamp}`;
const url = identifier ? `/v1/graph?type=geolocation&server=${identifier}&timestamp=${timestamp}` :
`/v1/graph?type=geolocation&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchOptimizedPerformance = async (timestamp, identifier) => {
const url = `/v1/graph?type=optimizedPerformance&server=${identifier}&timestamp=${timestamp}`;
export const fetchOptimizedPerformance = async (timestamp, identifier, after) => {
const url = `/v1/graph?type=optimizedPerformance&server=${identifier}&timestamp=${timestamp}&after=${after}`;
return doGetRequest(url);
}
@ -114,3 +116,15 @@ export const fetchPingGraph = async (timestamp, identifier) => {
const url = `/v1/graph?type=aggregatedPing&server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchJoinAddressPie = async (timestamp, identifier) => {
const url = identifier ? `/v1/graph?type=joinAddressPie&server=${identifier}&timestamp=${timestamp}` :
`/v1/graph?type=joinAddressPie&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchJoinAddressByDay = async (timestamp, identifier) => {
const url = identifier ? `/v1/graph?type=joinAddressByDay&server=${identifier}&timestamp=${timestamp}` :
`/v1/graph?type=joinAddressByDay&timestamp=${timestamp}`;
return doGetRequest(url);
}

View File

@ -1868,6 +1868,7 @@ a.text-dark:hover, a.text-dark:focus {
#wrapper {
display: flex;
min-height: 100vh;
}
#wrapper #content-wrapper {

View File

@ -1344,4 +1344,9 @@ button, input[type="submit"], input[type="reset"] {
.login-username {
position: relative;
top: 0.1rem;
}
.dataTables_filter input {
/* Fixes datatables search bar going outside cards */
width: calc(100% - 3.7rem) !important;
}

View File

@ -22,4 +22,99 @@ export const linegraphButtons = [{
export const tooltip = {
twoDecimals: {valueDecimals: 2},
zeroDecimals: {valueDecimals: 0}
}
export const mapPerformanceDataToSeries = performanceData => {
const playersOnline = [];
const tps = [];
const cpu = [];
const ram = [];
const entities = [];
const chunks = [];
const disk = [];
return new Promise((resolve => {
let i = 0;
const length = performanceData.length;
function processNextThousand() {
const to = Math.min(i + 1000, length);
for (i; i < to; i++) {
const entry = performanceData[i];
const date = entry[0];
playersOnline[i] = [date, entry[1]];
tps[i] = [date, entry[2]];
cpu[i] = [date, entry[3]];
ram[i] = [date, entry[4]];
entities[i] = [date, entry[5]];
chunks[i] = [date, entry[6]];
disk[i] = [date, entry[7]];
}
if (i >= length) {
resolve({playersOnline, tps, cpu, ram, entities, chunks, disk})
} else {
setTimeout(processNextThousand, 10);
}
}
processNextThousand();
}))
};
export const yAxisConfigurations = {
PLAYERS_ONLINE: {
labels: {
formatter: function () {
return this.value + ' P';
}
},
softMin: 0,
softMax: 2
},
TPS: {
opposite: true,
labels: {
formatter: function () {
return this.value + ' TPS';
}
},
softMin: 0,
softMax: 20
},
CPU: {
opposite: true,
labels: {
formatter: function () {
return this.value + '%';
}
},
softMin: 0,
softMax: 100
},
RAM_OR_DISK: {
labels: {
formatter: function () {
return this.value + ' MB';
}
},
softMin: 0
},
ENTITIES: {
opposite: true,
labels: {
formatter: function () {
return this.value + ' E';
}
},
softMin: 0,
softMax: 2
},
CHUNKS: {
labels: {
formatter: function () {
return this.value + ' C';
}
},
softMin: 0
}
}

View File

@ -0,0 +1,24 @@
import React from 'react';
import {Col, Row} from "react-bootstrap-v5";
import {ErrorViewCard} from "../ErrorView";
import GeolocationsCard from "../../components/cards/common/GeolocationsCard";
import PingTableCard from "../../components/cards/common/PingTableCard";
import LoadIn from "../../components/animation/LoadIn";
const Geolocations = ({className, geolocationData, pingData, geolocationError, pingError}) => {
return (
<LoadIn>
<section className={className}>
<Row>
<Col md={12}>
{geolocationError ? <ErrorViewCard error={geolocationError}/> :
<GeolocationsCard data={geolocationData}/>}
{pingError ? <ErrorViewCard error={pingError}/> : <PingTableCard data={pingData}/>}
</Col>
</Row>
</section>
</LoadIn>
)
};
export default Geolocations

View File

@ -8,6 +8,7 @@ import {
faCubes,
faGlobe,
faInfoCircle,
faLocationArrow,
faNetworkWired,
faSearch,
faServer,
@ -23,18 +24,15 @@ import {useMetadata} from "../../hooks/metadataHook";
import {faCalendarCheck} from "@fortawesome/free-regular-svg-icons";
import {SwitchTransition} from "react-transition-group";
import MainPageRedirect from "../../components/navigation/MainPageRedirect";
import ExtensionIcon from "../../components/extensions/ExtensionIcon";
import {ServerExtensionContextProvider, useServerExtensionContext} from "../../hooks/serverExtensionDataContext";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchNetworkMetadata} from "../../service/metadataService";
import {iconTypeToFontAwesomeClass} from "../../util/icons";
const NetworkSidebar = () => {
const {t, i18n} = useTranslation();
const {sidebarItems, setSidebarItems} = useNavigation();
const {networkMetadata} = useMetadata();
const {extensionData} = useServerExtensionContext();
const {data: networkMetadata} = useDataRequest(fetchNetworkMetadata, [])
useEffect(() => {
const servers = networkMetadata?.servers || [];
const items = [
@ -73,6 +71,7 @@ const NetworkSidebar = () => {
icon: faChartLine,
href: "playerbase"
},
{name: 'html.label.joinAddresses', icon: faLocationArrow, href: "join-addresses"},
// {name: 'html.label.playerRetention', icon: faUsersViewfinder, href: "retention"},
{name: 'html.label.playerList', icon: faUserGroup, href: "players"},
{name: 'html.label.geolocations', icon: faGlobe, href: "geolocations"},
@ -89,7 +88,7 @@ const NetworkSidebar = () => {
.map(info => {
return {
name: info.pluginName,
icon: <ExtensionIcon icon={info.icon}/>,
icon: [iconTypeToFontAwesomeClass(info.icon.family), info.icon.iconName],
href: `plugins/${encodeURIComponent(info.pluginName)}`
}
}).forEach(item => items.push(item))

View File

@ -10,6 +10,7 @@ import {
faCubes,
faGlobe,
faInfoCircle,
faLocationArrow,
faSearch,
faUserGroup,
faUsers
@ -26,8 +27,8 @@ import {SwitchTransition} from "react-transition-group";
import MainPageRedirect from "../../components/navigation/MainPageRedirect";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchServerIdentity} from "../../service/serverService";
import ExtensionIcon from "../../components/extensions/ExtensionIcon";
import {ServerExtensionContextProvider, useServerExtensionContext} from "../../hooks/serverExtensionDataContext";
import {iconTypeToFontAwesomeClass} from "../../util/icons";
const ServerSidebar = () => {
const {t, i18n} = useTranslation();
@ -68,6 +69,7 @@ const ServerSidebar = () => {
icon: faChartLine,
href: "playerbase"
},
{name: 'html.label.joinAddresses', icon: faLocationArrow, href: "join-addresses"},
// {name: 'html.label.playerRetention', icon: faUsersViewfinder, href: "retention"},
{name: 'html.label.playerList', icon: faUserGroup, href: "players"},
{name: 'html.label.geolocations', icon: faGlobe, href: "geolocations"},
@ -85,7 +87,7 @@ const ServerSidebar = () => {
.map(info => {
return {
name: info.pluginName,
icon: <ExtensionIcon icon={info.icon}/>,
icon: [iconTypeToFontAwesomeClass(info.icon.family), info.icon.iconName],
href: `plugins/${encodeURIComponent(info.pluginName)}`
}
}).forEach(item => items.push(item))

View File

@ -0,0 +1,19 @@
import React from 'react';
import {useDataRequest} from "../../hooks/dataFetchHook";
import Geolocations from "../common/Geolocations";
import {fetchNetworkPingTable} from "../../service/networkService";
import {fetchGeolocations} from "../../service/serverService";
const NetworkGeolocations = () => {
const {data, loadingError} = useDataRequest(fetchGeolocations, []);
const {data: pingData, loadingError: pingLoadingError} = useDataRequest(fetchNetworkPingTable, []);
return (
<Geolocations className={"network_geolocations"}
geolocationData={data} geolocationError={loadingError}
pingData={pingData} pingError={pingLoadingError}
/>
)
};
export default NetworkGeolocations

View File

@ -0,0 +1,19 @@
import React from 'react';
import {Col, Row} from "react-bootstrap-v5";
import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddressGroupCard";
import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard";
const NetworkJoinAddresses = () => {
return (
<Row>
<Col lg={8}>
<JoinAddressGraphCard identifier={undefined}/>
</Col>
<Col lg={4}>
<JoinAddressGroupCard identifier={undefined}/>
</Col>
</Row>
)
};
export default NetworkJoinAddresses

View File

@ -0,0 +1,119 @@
import React, {useCallback, useEffect, useState} from 'react';
import LoadIn from "../../components/animation/LoadIn";
import {Card, Col, Row} from "react-bootstrap-v5";
import {useMetadata} from "../../hooks/metadataHook";
import CardHeader from "../../components/cards/CardHeader";
import {faServer} from "@fortawesome/free-solid-svg-icons";
import MultiSelect from "../../components/input/MultiSelect";
import {useTranslation} from "react-i18next";
import {fetchOptimizedPerformance} from "../../service/serverService";
import {fetchNetworkPerformanceOverview} from "../../service/networkService";
import PerformanceAsNumbersCard from "../../components/cards/server/tables/PerformanceAsNumbersCard";
import {useNavigation} from "../../hooks/navigationHook";
import {mapPerformanceDataToSeries} from "../../util/graphs";
import PerformanceGraphsCard from "../../components/cards/network/PerformanceGraphsCard";
const NetworkPerformance = () => {
const {t} = useTranslation();
const {networkMetadata} = useMetadata();
const {updateRequested} = useNavigation();
const [serverOptions, setServerOptions] = useState([]);
const [selectedOptions, setSelectedOptions] = useState([]);
const [visualizedServers, setVisualizedServers] = useState([]);
const initializeServerOptions = () => {
if (networkMetadata) {
const options = networkMetadata.servers;
setServerOptions(options);
const indexOfProxy = options
.findIndex(option => option.serverName === networkMetadata.currentServer.serverName);
setSelectedOptions([indexOfProxy]);
setVisualizedServers([indexOfProxy]);
}
};
useEffect(initializeServerOptions, [networkMetadata, setVisualizedServers]);
const applySelected = () => {
setVisualizedServers(selectedOptions);
}
const [performanceData, setPerformanceData] = useState({});
const loadPerformanceData = useCallback(async () => {
const loaded = {
servers: [],
data: [],
values: [],
errors: [],
zones: {},
colors: {},
timestamp_f: ''
}
const time = new Date().getTime();
const monthMs = 2592000000;
const after = time - monthMs;
for (const index of visualizedServers) {
const server = serverOptions[index];
const {data, error} = await fetchOptimizedPerformance(time, encodeURIComponent(server.serverUUID), after);
if (data) {
loaded.servers.push(server);
const values = data.values;
delete data.values;
loaded.data.push(data);
loaded.values.push(await mapPerformanceDataToSeries(values));
loaded.zones = data.zones;
loaded.colors = data.colors;
loaded.timestamp_f = data.timestamp_f;
} else if (error) {
loaded.errors.push(error);
}
}
const selectedUUIDs = visualizedServers
.map(index => serverOptions[index])
.map(server => server.serverUUID);
const {data, error} = await fetchNetworkPerformanceOverview(time, selectedUUIDs);
if (error) loaded.errors.push(error);
setPerformanceData({...loaded, overview: data});
}, [visualizedServers, serverOptions, setPerformanceData])
useEffect(() => {
loadPerformanceData();
}, [loadPerformanceData, visualizedServers, updateRequested]);
const isUpToDate = visualizedServers.every((s, i) => s === selectedOptions[i]);
return (
<LoadIn>
<section className={"network_performance"}>
<Row>
<Col>
<PerformanceGraphsCard data={performanceData}/>
</Col>
</Row>
<Row>
<Col md={8}>
<PerformanceAsNumbersCard data={performanceData?.overview?.numbers}/>
</Col>
<Col md={4}>
<Card>
<CardHeader icon={faServer} color={'light-green'} label={t('html.label.serverSelector')}/>
<MultiSelect options={serverOptions.map(server => server.serverName)}
selectedIndexes={selectedOptions}
setSelectedIndexes={setSelectedOptions}/>
<button className={'btn bg-transparent'} onClick={applySelected} disabled={isUpToDate}>
{t('html.label.apply')}
</button>
</Card>
</Col>
</Row>
</section>
</LoadIn>
)
};
export default NetworkPerformance

View File

@ -0,0 +1,42 @@
import {Col, Row} from "react-bootstrap-v5";
import React from "react";
import PlayerbaseDevelopmentCard from "../../components/cards/server/graphs/PlayerbaseDevelopmentCard";
import CurrentPlayerbaseCard from "../../components/cards/server/graphs/CurrentPlayerbaseCard";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {ErrorViewCard} from "../ErrorView";
import PlayerbaseTrendsCard from "../../components/cards/server/tables/PlayerbaseTrendsCard";
import PlayerbaseInsightsCard from "../../components/cards/server/insights/PlayerbaseInsightsCard";
import LoadIn from "../../components/animation/LoadIn";
import {fetchNetworkPlayerbaseOverview} from "../../service/networkService";
const NetworkPlayerbaseOverview = () => {
const {data, loadingError} = useDataRequest(fetchNetworkPlayerbaseOverview, []);
return (
<LoadIn>
<section className="network_playerbase">
<Row>
<Col lg={8}>
<PlayerbaseDevelopmentCard identifier={undefined}/>
</Col>
<Col lg={4}>
<CurrentPlayerbaseCard identifier={undefined}/>
</Col>
</Row>
<Row>
{loadingError && <ErrorViewCard error={loadingError}/>}
{!loadingError && <>
<Col lg={8}>
<PlayerbaseTrendsCard data={data?.trends}/>
</Col>
<Col lg={4}>
<PlayerbaseInsightsCard data={data?.insights}/>
</Col>
</>}
</Row>
</section>
</LoadIn>
)
}
export default NetworkPlayerbaseOverview;

View File

@ -0,0 +1,32 @@
import React, {useState} from 'react';
import {Col, Row} from "react-bootstrap-v5";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchServersOverview} from "../../service/networkService";
import ErrorView from "../ErrorView";
import ServersTableCard from "../../components/cards/network/ServersTableCard";
import QuickViewGraphCard from "../../components/cards/network/QuickViewGraphCard";
import QuickViewDataCard from "../../components/cards/network/QuickViewDataCard";
const NetworkServers = () => {
const [selectedServer, setSelectedServer] = useState(0);
const {data, loadingError} = useDataRequest(fetchServersOverview, [])
if (loadingError) {
return <ErrorView error={loadingError}/>
}
return (
<Row>
<Col md={6}>
<ServersTableCard servers={data?.servers || []} onSelect={(index) => setSelectedServer(index)}/>
</Col>
<Col md={6}>
{data?.servers.length && <QuickViewGraphCard server={data.servers[selectedServer]}/>}
{data?.servers.length && <QuickViewDataCard server={data.servers[selectedServer]}/>}
</Col>
</Row>
)
};
export default NetworkServers

View File

@ -0,0 +1,26 @@
import {Col, Row} from "react-bootstrap-v5";
import React from "react";
import ServerRecentSessionsCard from "../../components/cards/server/tables/ServerRecentSessionsCard";
import SessionInsightsCard from "../../components/cards/server/insights/SessionInsightsCard";
import LoadIn from "../../components/animation/LoadIn";
import ServerPieCard from "../../components/cards/common/ServerPieCard";
const NetworkSessions = () => {
return (
<LoadIn>
<section className="server_sessions">
<Row>
<Col lg={8}>
<ServerRecentSessionsCard identifier={undefined}/>
</Col>
<Col lg={4}>
<ServerPieCard/>
<SessionInsightsCard identifier={undefined}/>
</Col>
</Row>
</section>
</LoadIn>
)
}
export default NetworkSessions;

View File

@ -20,10 +20,10 @@ const PlayerbaseOverview = () => {
<section className="server_playerbase">
<Row>
<Col lg={8}>
<PlayerbaseDevelopmentCard/>
<PlayerbaseDevelopmentCard identifier={identifier}/>
</Col>
<Col lg={4}>
<CurrentPlayerbaseCard/>
<CurrentPlayerbaseCard identifier={identifier}/>
</Col>
</Row>
<Row>

View File

@ -2,11 +2,7 @@ import React from 'react';
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchGeolocations, fetchPingTable} from "../../service/serverService";
import {Col, Row} from "react-bootstrap-v5";
import {ErrorViewCard} from "../ErrorView";
import GeolocationsCard from "../../components/cards/common/GeolocationsCard";
import LoadIn from "../../components/animation/LoadIn";
import PingTableCard from "../../components/cards/common/PingTableCard";
import Geolocations from "../common/Geolocations";
const ServerGeolocations = () => {
const {identifier} = useParams();
@ -15,17 +11,10 @@ const ServerGeolocations = () => {
const {pingData, pingLoadingError} = useDataRequest(fetchPingTable, [identifier]);
return (
<LoadIn>
<section className="server_geolocations">
<Row>
<Col md={12}>
{loadingError ? <ErrorViewCard error={loadingError}/> : <GeolocationsCard data={data}/>}
{pingLoadingError ? <ErrorViewCard error={pingLoadingError}/> :
<PingTableCard data={pingData}/>}
</Col>
</Row>
</section>
</LoadIn>
<Geolocations className={"server_geolocations"}
geolocationData={data} geolocationError={loadingError}
pingData={pingData} pingError={pingLoadingError}
/>
)
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import {Col, Row} from "react-bootstrap-v5";
import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddressGroupCard";
import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard";
import {useParams} from "react-router-dom";
const ServerJoinAddresses = () => {
const {identifier} = useParams();
return (
<Row>
<Col lg={8}>
<JoinAddressGraphCard identifier={identifier}/>
</Col>
<Col lg={4}>
<JoinAddressGroupCard identifier={identifier}/>
</Col>
</Row>
)
};
export default ServerJoinAddresses

View File

@ -4,18 +4,20 @@ import ServerWorldPieCard from "../../components/cards/server/graphs/ServerWorld
import ServerRecentSessionsCard from "../../components/cards/server/tables/ServerRecentSessionsCard";
import SessionInsightsCard from "../../components/cards/server/insights/SessionInsightsCard";
import LoadIn from "../../components/animation/LoadIn";
import {useParams} from "react-router-dom";
const ServerSessions = () => {
const {identifier} = useParams();
return (
<LoadIn>
<section className="server_sessions">
<Row>
<Col lg={8}>
<ServerRecentSessionsCard/>
<ServerRecentSessionsCard identifier={identifier}/>
</Col>
<Col lg={4}>
<ServerWorldPieCard/>
<SessionInsightsCard/>
<SessionInsightsCard identifier={identifier}/>
</Col>
</Row>
</section>

View File

@ -2879,10 +2879,10 @@ boolbase@^1.0.0, boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
bootstrap@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.0.tgz#838727fb60f1630db370fe57c63cbcf2962bb3d3"
integrity sha512-qlnS9GL6YZE6Wnef46GxGv1UpGGzAwO0aPL1yOjzDIJpeApeMvqV24iL+pjr2kU4dduoBA9fINKWKgMToobx9A==
bootstrap@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.1.tgz#45f97ff05cbe828bad807b014d8425f3aeb8ec3a"
integrity sha512-UQi3v2NpVPEi1n35dmRRzBJFlgvWHYwyem6yHhuT6afYF+sziEt46McRbT//kVXZ7b1YUYEVGdXEH74Nx3xzGA==
brace-expansion@^1.1.7:
version "1.1.11"
@ -8135,10 +8135,10 @@ sass-loader@^12.3.0:
klona "^2.0.4"
neo-async "^2.6.2"
sass@^1.54.8:
version "1.54.8"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.8.tgz#4adef0dd86ea2b1e4074f551eeda4fc5f812a996"
integrity sha512-ib4JhLRRgbg6QVy6bsv5uJxnJMTS2soVcCp9Y88Extyy13A8vV0G1fAwujOzmNkFQbR3LvedudAMbtuNRPbQww==
sass@^1.54.9:
version "1.54.9"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.9.tgz#b05f14ed572869218d1a76961de60cd647221762"
integrity sha512-xb1hjASzEH+0L0WI9oFjqhRi51t/gagWnxLiwUNMltA0Ab6jIDkAacgKiGYKM9Jhy109osM7woEEai6SXeJo5Q==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"