diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_CN.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_CN.yml index 1517531c3..0483b95aa 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_CN.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_CN.yml @@ -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: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_CS.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_CS.yml index 23f5c539e..933a32263 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_CS.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_CS.yml @@ -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'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_DE.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_DE.yml index 5ca0a58f0..0140fc746 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_DE.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_DE.yml @@ -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'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_EN.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_EN.yml index 4e6e9e59b..3abca7543 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_EN.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_EN.yml @@ -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'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_ES.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_ES.yml index 20bf56d17..c677bb9c7 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_ES.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_ES.yml @@ -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'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_FI.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_FI.yml index c5816928c..a0dad6c4d 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_FI.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_FI.yml @@ -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'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_FR.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_FR.yml index f6c7d6ea1..aa643263d 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_FR.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_FR.yml @@ -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'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_IT.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_IT.yml index 2aef34f0e..c81690b6d 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_IT.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_IT.yml @@ -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'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_JA.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_JA.yml index ca87f4128..f8be0226a 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_JA.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_JA.yml @@ -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: "'月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_KO.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_KO.yml index 40a5bae2b..2e92a21c4 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_KO.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_KO.yml @@ -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: "'월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_NL.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_NL.yml index 3ea140733..5d05f8183 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_NL.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_NL.yml @@ -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'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_PT_BR.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_PT_BR.yml index 83c122b47..7202f190e 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_PT_BR.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_PT_BR.yml @@ -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'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_RU.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_RU.yml index 25739c8ff..e58660b08 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_RU.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_RU.yml @@ -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: "'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_TR.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_TR.yml index e3ee0fd6a..b18397d57 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_TR.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_TR.yml @@ -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'" diff --git a/Plan/src/main/resources/assets/plan/plan/locale/locale_ZH_TW.yml b/Plan/src/main/resources/assets/plan/plan/locale/locale_ZH_TW.yml index d6401c654..30f2d9b2c 100644 --- a/Plan/src/main/resources/assets/plan/plan/locale/locale_ZH_TW.yml +++ b/Plan/src/main/resources/assets/plan/plan/locale/locale_ZH_TW.yml @@ -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: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'" diff --git a/Plan/src/main/resources/assets/plan/plan/web/css/sb-admin-2.css b/Plan/src/main/resources/assets/plan/plan/web/css/sb-admin-2.css index 8807d8b82..b58acd7cd 100644 --- a/Plan/src/main/resources/assets/plan/plan/web/css/sb-admin-2.css +++ b/Plan/src/main/resources/assets/plan/plan/web/css/sb-admin-2.css @@ -1866,6 +1866,7 @@ a.text-dark:hover, a.text-dark:focus { #wrapper { display: flex; + min-height: 100vh; } #wrapper #content-wrapper { diff --git a/react/dashboard/dashboard/package.json b/react/dashboard/dashboard/package.json index 06b225dc1..ac72290f5 100644 --- a/react/dashboard/dashboard/package.json +++ b/react/dashboard/dashboard/package.json @@ -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" diff --git a/react/dashboard/dashboard/src/App.js b/react/dashboard/dashboard/src/App.js index c14fe27a5..699f7f45b 100644 --- a/react/dashboard/dashboard/src/App.js +++ b/react/dashboard/dashboard/src/App.js @@ -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() { }/> }/> }/> + }/> }/> }/> }/> @@ -115,7 +123,13 @@ function App() { }> }/> }/> + }/> + }/> + }/> + }/> + }/> }/> + }/> }/> }/> { const SessionAccordion = ( { sessions, - isPlayer + isPlayer, + isNetwork } ) => { const {t} = useTranslation(); @@ -99,7 +100,10 @@ const SessionAccordion = ( firstColumn, <> {t('html.label.sessionStart')}, <> {t('html.label.length')}, - <> {t('html.label.mostPlayedWorld')} + <> + {!isNetwork && <> {t('html.label.mostPlayedWorld')}} + {isNetwork && <> {t('html.label.server')}} + ]} slices={sessions.map(session => { return { body: , diff --git a/react/dashboard/dashboard/src/components/cards/common/GeolocationsCard.js b/react/dashboard/dashboard/src/components/cards/common/GeolocationsCard.js index 88b49fd0c..ebca1fecb 100644 --- a/react/dashboard/dashboard/src/components/cards/common/GeolocationsCard.js +++ b/react/dashboard/dashboard/src/components/cards/common/GeolocationsCard.js @@ -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 ( + + + {t(projection)} + + + +
{t('html.label.geoProjection.dropdown')}
+ {projectionOptions.map((option, i) => ( + setProjection(option)}> + {t(option)} + + ))} +
+
+ ) +} const GeolocationsCard = ({data}) => { const {t} = useTranslation(); + const [projection, setProjection] = useState(ProjectionOptions.MILLER); if (!data) return if (!data?.geolocations_enabled) { return (
- {' '} - {t('html.description.noGeolocations')} + {' '}{t('html.description.noGeolocations')}
) } @@ -27,6 +54,7 @@ const GeolocationsCard = ({data}) => {
{t('html.label.geolocations')}
+ @@ -34,7 +62,8 @@ const GeolocationsCard = ({data}) => { - + diff --git a/react/dashboard/dashboard/src/components/cards/common/RecentSessionsCard.js b/react/dashboard/dashboard/src/components/cards/common/RecentSessionsCard.js index 8c54cc87d..4c6042cd2 100644 --- a/react/dashboard/dashboard/src/components/cards/common/RecentSessionsCard.js +++ b/react/dashboard/dashboard/src/components/cards/common/RecentSessionsCard.js @@ -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 ( @@ -19,7 +19,7 @@ const RecentSessionsCard = ({sessions, isPlayer}) => { - + ) diff --git a/react/dashboard/dashboard/src/components/cards/common/ServerPieCard.js b/react/dashboard/dashboard/src/components/cards/common/ServerPieCard.js new file mode 100644 index 000000000..250fcc9a8 --- /dev/null +++ b/react/dashboard/dashboard/src/components/cards/common/ServerPieCard.js @@ -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 ; + if (loadingError) return ; + + const series = data.server_pie_series_30d; + const colors = data.server_pie_colors; + + return ( + + + + + ) +} + +export default ServerPieCard; \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/cards/network/PerformanceGraphsCard.js b/react/dashboard/dashboard/src/components/cards/network/PerformanceGraphsCard.js new file mode 100644 index 000000000..e28774a0b --- /dev/null +++ b/react/dashboard/dashboard/src/components/cards/network/PerformanceGraphsCard.js @@ -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 ( + + ) +} + +const PingTab = ({identifier}) => { + const {data, loadingError} = useDataRequest(fetchPingGraph, [identifier]); + + if (loadingError) return + if (!data) return ; + + return ; +} + +const PerformanceGraphsCard = ({data}) => { + const {t} = useTranslation(); + const {networkMetadata} = useMetadata(); + + if (!data || !Object.values(data).length) return + + 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 + } + + return ( + + + }, { + name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps', + element: + }, { + name: t('html.label.cpu'), icon: faTachometerAlt, color: 'amber', href: 'cpu', + element: + }, { + name: t('html.label.ram'), icon: faMicrochip, color: 'light-green', href: 'ram', + element: + }, { + name: t('html.label.entities'), icon: faDragon, color: 'purple', href: 'entities', + element: + }, { + name: t('html.label.loadedChunks'), icon: faMap, color: 'blue-grey', href: 'chunks', + element: + }, { + name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk', + element: + }, { + name: t('html.label.ping'), icon: faSignal, color: 'amber', href: 'ping', + element: networkMetadata ? : + + }, + ]}/> + + ) +}; + +export default PerformanceGraphsCard \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/cards/network/QuickViewDataCard.js b/react/dashboard/dashboard/src/components/cards/network/QuickViewDataCard.js new file mode 100644 index 000000000..3aad6395f --- /dev/null +++ b/react/dashboard/dashboard/src/components/cards/network/QuickViewDataCard.js @@ -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 ( + + + + + + +
+

{t('html.label.last7days')}

+ + + + + +
+
+ ) +}; + +export default QuickViewDataCard \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/cards/network/QuickViewGraphCard.js b/react/dashboard/dashboard/src/components/cards/network/QuickViewGraphCard.js new file mode 100644 index 000000000..7706c5cda --- /dev/null +++ b/react/dashboard/dashboard/src/components/cards/network/QuickViewGraphCard.js @@ -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 ( + + + + + ) +}; + +export default QuickViewGraphCard \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/cards/network/ServersTableCard.js b/react/dashboard/dashboard/src/components/cards/network/ServersTableCard.js new file mode 100644 index 000000000..4050455fe --- /dev/null +++ b/react/dashboard/dashboard/src/components/cards/network/ServersTableCard.js @@ -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 ( + + + {t(sortBy.label)} + + + +
{t('html.label.sortBy')}
+ {sortOptions.map((option, i) => ( + setSortBy(option)}> + {t(option.label)} + + ))} +
+
+ ) +} + +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 ( + + +
+ {t('html.label.servers')} +
+ +
+ {!servers.length && +

No servers found in the database.

+

It appears that Plan is not installed on any game servers or not connected to the same database. + See wiki for Network tutorial.

+
} + {servers.length && } +
+ ) +}; + +export default ServersTableCard \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/cards/server/graphs/CurrentPlayerbaseCard.js b/react/dashboard/dashboard/src/components/cards/server/graphs/CurrentPlayerbaseCard.js index bba7af52b..897805e18 100644 --- a/react/dashboard/dashboard/src/components/cards/server/graphs/CurrentPlayerbaseCard.js +++ b/react/dashboard/dashboard/src/components/cards/server/graphs/CurrentPlayerbaseCard.js @@ -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 = () => { {t('html.label.currentPlayerbase')} - + ) } diff --git a/react/dashboard/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.js b/react/dashboard/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.js new file mode 100644 index 000000000..22382fc8a --- /dev/null +++ b/react/dashboard/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.js @@ -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 + if (!data) return ; + + + return ( + + +
+ {t('html.label.joinAddresses')} +
+ {t('html.label.stacked')} +
+ +
+ ) +}; + +export default JoinAddressGraphCard \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.js b/react/dashboard/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.js new file mode 100644 index 000000000..952758b01 --- /dev/null +++ b/react/dashboard/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.js @@ -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 + if (!data) return ; + + return ( + + +
+ {t('html.label.latestJoinAddresses')} +
+
+ +
+ ) +}; + +export default JoinAddressGroupCard \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js b/react/dashboard/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js index baa0e6ed2..d094ff15d 100644 --- a/react/dashboard/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js +++ b/react/dashboard/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js @@ -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 @@ -58,43 +59,6 @@ const PingGraphTab = ({identifier}) => { return ; } -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]); diff --git a/react/dashboard/dashboard/src/components/cards/server/graphs/PlayerbaseDevelopmentCard.js b/react/dashboard/dashboard/src/components/cards/server/graphs/PlayerbaseDevelopmentCard.js index edf74022b..9f114ca02 100644 --- a/react/dashboard/dashboard/src/components/cards/server/graphs/PlayerbaseDevelopmentCard.js +++ b/react/dashboard/dashboard/src/components/cards/server/graphs/PlayerbaseDevelopmentCard.js @@ -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, diff --git a/react/dashboard/dashboard/src/components/cards/server/insights/SessionInsightsCard.js b/react/dashboard/dashboard/src/components/cards/server/insights/SessionInsightsCard.js index a98fc452c..961ddcad8 100644 --- a/react/dashboard/dashboard/src/components/cards/server/insights/SessionInsightsCard.js +++ b/react/dashboard/dashboard/src/components/cards/server/insights/SessionInsightsCard.js @@ -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 + const insights = data?.insights; + return ( ) diff --git a/react/dashboard/dashboard/src/components/cards/server/tables/ServerRecentSessionsCard.js b/react/dashboard/dashboard/src/components/cards/server/tables/ServerRecentSessionsCard.js index 198c21988..8f406fb98 100644 --- a/react/dashboard/dashboard/src/components/cards/server/tables/ServerRecentSessionsCard.js +++ b/react/dashboard/dashboard/src/components/cards/server/tables/ServerRecentSessionsCard.js @@ -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 return ( - + ) } diff --git a/react/dashboard/dashboard/src/components/extensions/ExtensionIcon.js b/react/dashboard/dashboard/src/components/extensions/ExtensionIcon.js index 564c10c4a..6f424d957 100644 --- a/react/dashboard/dashboard/src/components/extensions/ExtensionIcon.js +++ b/react/dashboard/dashboard/src/components/extensions/ExtensionIcon.js @@ -13,8 +13,8 @@ const ExtensionIcon = ({icon}) => { ) } -export const toExtensionIconHtmlString = ({icon}) => { - return icon ? `` : ''; +export const toExtensionIconHtmlString = (icon) => { + return icon ? `` : ''; } export default ExtensionIcon; diff --git a/react/dashboard/dashboard/src/components/graphs/GeolocationWorldMap.js b/react/dashboard/dashboard/src/components/graphs/GeolocationWorldMap.js index 55934ac2c..691f95a41 100644 --- a/react/dashboard/dashboard/src/components/graphs/GeolocationWorldMap.js +++ b/react/dashboard/dashboard/src/components/graphs/GeolocationWorldMap.js @@ -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 (
); }; diff --git a/react/dashboard/dashboard/src/components/graphs/GroupBarGraph.js b/react/dashboard/dashboard/src/components/graphs/GroupBarGraph.js new file mode 100644 index 000000000..e77057713 --- /dev/null +++ b/react/dashboard/dashboard/src/components/graphs/GroupBarGraph.js @@ -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 (
); +}; + +export default GroupBarGraph \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/graphs/PlayerbasePie.js b/react/dashboard/dashboard/src/components/graphs/GroupPie.js similarity index 52% rename from react/dashboard/dashboard/src/components/graphs/PlayerbasePie.js rename to react/dashboard/dashboard/src/components/graphs/GroupPie.js index b5c71d447..818cbcf28 100644 --- a/react/dashboard/dashboard/src/components/graphs/PlayerbasePie.js +++ b/react/dashboard/dashboard/src/components/graphs/GroupPie.js @@ -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 '' + this.point.name + ': ' + this.y; + } + }, series: [pieSeries] }); - }, [series, graphTheming, nightModeEnabled, t]); + }, [id, colors, groups, name, graphTheming, nightModeEnabled, t]); - return (
); -} + return (
); +}; -export default PlayerbasePie; \ No newline at end of file +export default GroupPie; \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/graphs/GroupVisualizer.js b/react/dashboard/dashboard/src/components/graphs/GroupVisualizer.js new file mode 100644 index 000000000..d82488121 --- /dev/null +++ b/react/dashboard/dashboard/src/components/graphs/GroupVisualizer.js @@ -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 + case options.PIE: + return + case options.BAR: + return ; + case options.COLUMN: + default: + return ; + } +} + +const VisualizerSelector = ({onClick, icon}) => { + return ( + + ) +} + +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 + + setVisualization(options.PIE)}/> + setVisualization(options.TABLE)}/> + setVisualization(horizontal ? options.BAR : options.COLUMN)}/> + + + + + +}; + +export default GroupVisualizer \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/graphs/JoinAddressGraph.js b/react/dashboard/dashboard/src/components/graphs/JoinAddressGraph.js new file mode 100644 index 000000000..079b7c251 --- /dev/null +++ b/react/dashboard/dashboard/src/components/graphs/JoinAddressGraph.js @@ -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 ( +
+ +
+ ) +}; + +export default JoinAddressGraph \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/graphs/LineGraph.js b/react/dashboard/dashboard/src/components/graphs/LineGraph.js index 6d05db467..3a8e0b2e5 100644 --- a/react/dashboard/dashboard/src/components/graphs/LineGraph.js +++ b/react/dashboard/dashboard/src/components/graphs/LineGraph.js @@ -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 ( -
+
) diff --git a/react/dashboard/dashboard/src/components/graphs/ServerPie.js b/react/dashboard/dashboard/src/components/graphs/ServerPie.js index 88824a2d3..c13536685 100644 --- a/react/dashboard/dashboard/src/components/graphs/ServerPie.js +++ b/react/dashboard/dashboard/src/components/graphs/ServerPie.js @@ -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', diff --git a/react/dashboard/dashboard/src/components/input/BasicDropdown.js b/react/dashboard/dashboard/src/components/input/BasicDropdown.js new file mode 100644 index 000000000..6aa213e1e --- /dev/null +++ b/react/dashboard/dashboard/src/components/input/BasicDropdown.js @@ -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 ( + + + {t(optionLabelMapper ? optionLabelMapper(selected) : selected)} + + + +
{t(title)}
+ {optionList.map((option, i) => ( + onChange(option)}> + {t(optionLabelMapper ? optionLabelMapper(option) : option)} + + ))} +
+
+ ) +}; + +export default BasicDropdown \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/input/MultiSelect.js b/react/dashboard/dashboard/src/components/input/MultiSelect.js new file mode 100644 index 000000000..a483453e4 --- /dev/null +++ b/react/dashboard/dashboard/src/components/input/MultiSelect.js @@ -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 ( + + ) +}; + +export default MultiSelect \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/input/Toggle.js b/react/dashboard/dashboard/src/components/input/Toggle.js new file mode 100644 index 000000000..e9239c029 --- /dev/null +++ b/react/dashboard/dashboard/src/components/input/Toggle.js @@ -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 ( +
+ + +
+ ) +}; + +export default Toggle \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/navigation/Header.js b/react/dashboard/dashboard/src/components/navigation/Header.js index 8429d5e45..65b7d118f 100644 --- a/react/dashboard/dashboard/src/components/navigation/Header.js +++ b/react/dashboard/dashboard/src/components/navigation/Header.js @@ -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}) => {
{' '} {lastUpdate.formatted} diff --git a/react/dashboard/dashboard/src/components/table/GroupTable.js b/react/dashboard/dashboard/src/components/table/GroupTable.js new file mode 100644 index 000000000..686c95856 --- /dev/null +++ b/react/dashboard/dashboard/src/components/table/GroupTable.js @@ -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 ( + + {group.name} + {group.y} + + ) +} + +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 ( + + + + {groups.length ? groups.map((group, i) => + ) : + + + + + + } + +
{t('generic.noData')}---
+
+ ) +}; + +export default GroupTable \ No newline at end of file diff --git a/react/dashboard/dashboard/src/components/table/PerformanceAsNumbersTable.js b/react/dashboard/dashboard/src/components/table/PerformanceAsNumbersTable.js index ef4fc8dc4..82bd065da 100644 --- a/react/dashboard/dashboard/src/components/table/PerformanceAsNumbersTable.js +++ b/react/dashboard/dashboard/src/components/table/PerformanceAsNumbersTable.js @@ -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 ; + if (!data) return ; return ( { data.low_tps_spikes_24h ]}/> + { return ( @@ -18,24 +19,26 @@ const PingTable = ({countries}) => { const {nightModeEnabled} = useTheme(); return ( - - - - - - - - - - - {countries.length ? countries.map((country, i) => ) : - - - - - } - -
{t('html.label.country')}{t('html.label.averagePing')}{t('html.label.bestPing')}{t('html.label.worstPing')}
{t('generic.noData')}---
+ + + + + + + + + + + + {countries.length ? countries.map((country, i) => ) : + + + + + } + +
{t('html.label.country')}{t('html.label.averagePing')}{t('html.label.bestPing')}{t('html.label.worstPing')}
{t('generic.noData')}---
+
) }; diff --git a/react/dashboard/dashboard/src/components/table/ServersTable.js b/react/dashboard/dashboard/src/components/table/ServersTable.js new file mode 100644 index 000000000..17dadf1d5 --- /dev/null +++ b/react/dashboard/dashboard/src/components/table/ServersTable.js @@ -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 ( + + {server.name} + + {t('html.label.serverAnalysis')} + + + {server.players} + {server.online} + + + + + ); +} + +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 ( + + + + + + + + + + + + + {sortedServers.length ? sortedServers.map((server, i) => onSelect(servers.indexOf(server))}/>) : + + + + + + } + +
{t('html.label.server')} {t('html.label.serverAnalysis')} {t('html.label.registeredPlayers')} {t('html.label.playersOnline')}
{t('html.generic.none')}---
+
+ ) +}; + +export default ServersTable; \ No newline at end of file diff --git a/react/dashboard/dashboard/src/hooks/dataFetchHook.js b/react/dashboard/dashboard/src/hooks/dataFetchHook.js index a9a93e2c2..b0211014b 100644 --- a/react/dashboard/dashboard/src/hooks/dataFetchHook.js +++ b/react/dashboard/dashboard/src/hooks/dataFetchHook.js @@ -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}; diff --git a/react/dashboard/dashboard/src/hooks/datastoreHook.js b/react/dashboard/dashboard/src/hooks/datastoreHook.js new file mode 100644 index 000000000..ab840bede --- /dev/null +++ b/react/dashboard/dashboard/src/hooks/datastoreHook.js @@ -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}; +} \ No newline at end of file diff --git a/react/dashboard/dashboard/src/hooks/metadataHook.js b/react/dashboard/dashboard/src/hooks/metadataHook.js index b080f4f93..69222a8c8 100644 --- a/react/dashboard/dashboard/src/hooks/metadataHook.js +++ b/react/dashboard/dashboard/src/hooks/metadataHook.js @@ -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 ( {children} diff --git a/react/dashboard/dashboard/src/hooks/navigationHook.js b/react/dashboard/dashboard/src/hooks/navigationHook.js index d6aa72e8f..17c1154d9 100644 --- a/react/dashboard/dashboard/src/hooks/navigationHook.js +++ b/react/dashboard/dashboard/src/hooks/navigationHook.js @@ -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); diff --git a/react/dashboard/dashboard/src/service/networkService.js b/react/dashboard/dashboard/src/service/networkService.js index 9110a35a5..b37b546ec 100644 --- a/react/dashboard/dashboard/src/service/networkService.js +++ b/react/dashboard/dashboard/src/service/networkService.js @@ -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×tamp=${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))}×tamp=${timestamp}`; + return doGetRequest(url); } \ No newline at end of file diff --git a/react/dashboard/dashboard/src/service/serverService.js b/react/dashboard/dashboard/src/service/serverService.js index 88d8f53a1..eeb4a6e39 100644 --- a/react/dashboard/dashboard/src/service/serverService.js +++ b/react/dashboard/dashboard/src/service/serverService.js @@ -42,7 +42,8 @@ export const fetchExtensionData = async (timestamp, identifier) => { } export const fetchSessions = async (timestamp, identifier) => { - const url = `/v1/sessions?server=${identifier}×tamp=${timestamp}`; + const url = identifier ? `/v1/sessions?server=${identifier}×tamp=${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}×tamp=${timestamp}` : `/v1/pingTable?timestamp=${timestamp}`; + const url = `/v1/pingTable?server=${identifier}×tamp=${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}×tamp=${timestamp}`; + const url = identifier ? `/v1/graph?type=geolocation&server=${identifier}×tamp=${timestamp}` : + `/v1/graph?type=geolocation×tamp=${timestamp}`; return doGetRequest(url); } -export const fetchOptimizedPerformance = async (timestamp, identifier) => { - const url = `/v1/graph?type=optimizedPerformance&server=${identifier}×tamp=${timestamp}`; +export const fetchOptimizedPerformance = async (timestamp, identifier, after) => { + const url = `/v1/graph?type=optimizedPerformance&server=${identifier}×tamp=${timestamp}&after=${after}`; return doGetRequest(url); } @@ -114,3 +116,15 @@ export const fetchPingGraph = async (timestamp, identifier) => { const url = `/v1/graph?type=aggregatedPing&server=${identifier}×tamp=${timestamp}`; return doGetRequest(url); } + +export const fetchJoinAddressPie = async (timestamp, identifier) => { + const url = identifier ? `/v1/graph?type=joinAddressPie&server=${identifier}×tamp=${timestamp}` : + `/v1/graph?type=joinAddressPie×tamp=${timestamp}`; + return doGetRequest(url); +} + +export const fetchJoinAddressByDay = async (timestamp, identifier) => { + const url = identifier ? `/v1/graph?type=joinAddressByDay&server=${identifier}×tamp=${timestamp}` : + `/v1/graph?type=joinAddressByDay×tamp=${timestamp}`; + return doGetRequest(url); +} diff --git a/react/dashboard/dashboard/src/style/sb-admin-2.css b/react/dashboard/dashboard/src/style/sb-admin-2.css index 34d4ea61c..ee62c6d86 100644 --- a/react/dashboard/dashboard/src/style/sb-admin-2.css +++ b/react/dashboard/dashboard/src/style/sb-admin-2.css @@ -1868,6 +1868,7 @@ a.text-dark:hover, a.text-dark:focus { #wrapper { display: flex; + min-height: 100vh; } #wrapper #content-wrapper { diff --git a/react/dashboard/dashboard/src/style/style.css b/react/dashboard/dashboard/src/style/style.css index 9d8a38c3e..49e510ffc 100644 --- a/react/dashboard/dashboard/src/style/style.css +++ b/react/dashboard/dashboard/src/style/style.css @@ -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; } \ No newline at end of file diff --git a/react/dashboard/dashboard/src/util/graphs.js b/react/dashboard/dashboard/src/util/graphs.js index 2510bc7ab..da5e83b07 100644 --- a/react/dashboard/dashboard/src/util/graphs.js +++ b/react/dashboard/dashboard/src/util/graphs.js @@ -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 + } } \ No newline at end of file diff --git a/react/dashboard/dashboard/src/views/common/Geolocations.js b/react/dashboard/dashboard/src/views/common/Geolocations.js new file mode 100644 index 000000000..bea249143 --- /dev/null +++ b/react/dashboard/dashboard/src/views/common/Geolocations.js @@ -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 ( + +
+ + + {geolocationError ? : + } + {pingError ? : } + + +
+
+ ) +}; + +export default Geolocations \ No newline at end of file diff --git a/react/dashboard/dashboard/src/views/layout/NetworkPage.js b/react/dashboard/dashboard/src/views/layout/NetworkPage.js index d420341f8..8a7a85bb5 100644 --- a/react/dashboard/dashboard/src/views/layout/NetworkPage.js +++ b/react/dashboard/dashboard/src/views/layout/NetworkPage.js @@ -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: , + icon: [iconTypeToFontAwesomeClass(info.icon.family), info.icon.iconName], href: `plugins/${encodeURIComponent(info.pluginName)}` } }).forEach(item => items.push(item)) diff --git a/react/dashboard/dashboard/src/views/layout/ServerPage.js b/react/dashboard/dashboard/src/views/layout/ServerPage.js index be3a52d73..414185ad2 100644 --- a/react/dashboard/dashboard/src/views/layout/ServerPage.js +++ b/react/dashboard/dashboard/src/views/layout/ServerPage.js @@ -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: , + icon: [iconTypeToFontAwesomeClass(info.icon.family), info.icon.iconName], href: `plugins/${encodeURIComponent(info.pluginName)}` } }).forEach(item => items.push(item)) diff --git a/react/dashboard/dashboard/src/views/network/NetworkGeolocations.js b/react/dashboard/dashboard/src/views/network/NetworkGeolocations.js new file mode 100644 index 000000000..0b6bb884a --- /dev/null +++ b/react/dashboard/dashboard/src/views/network/NetworkGeolocations.js @@ -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 ( + + ) +}; + +export default NetworkGeolocations \ No newline at end of file diff --git a/react/dashboard/dashboard/src/views/network/NetworkJoinAddresses.js b/react/dashboard/dashboard/src/views/network/NetworkJoinAddresses.js new file mode 100644 index 000000000..358917d2b --- /dev/null +++ b/react/dashboard/dashboard/src/views/network/NetworkJoinAddresses.js @@ -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 ( + + + + + + + + + ) +}; + +export default NetworkJoinAddresses \ No newline at end of file diff --git a/react/dashboard/dashboard/src/views/network/NetworkPerformance.js b/react/dashboard/dashboard/src/views/network/NetworkPerformance.js new file mode 100644 index 000000000..8c1ec8ca9 --- /dev/null +++ b/react/dashboard/dashboard/src/views/network/NetworkPerformance.js @@ -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 ( + +
+ + + + + + + + + + + + + server.serverName)} + selectedIndexes={selectedOptions} + setSelectedIndexes={setSelectedOptions}/> + + + + +
+
+ ) +}; + +export default NetworkPerformance \ No newline at end of file diff --git a/react/dashboard/dashboard/src/views/network/NetworkPlayerbaseOverview.js b/react/dashboard/dashboard/src/views/network/NetworkPlayerbaseOverview.js new file mode 100644 index 000000000..e76fd104f --- /dev/null +++ b/react/dashboard/dashboard/src/views/network/NetworkPlayerbaseOverview.js @@ -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 ( + +
+ + + + + + + + + + {loadingError && } + {!loadingError && <> + + + + + + + } + +
+
+ ) +} + +export default NetworkPlayerbaseOverview; \ No newline at end of file diff --git a/react/dashboard/dashboard/src/views/network/NetworkServers.js b/react/dashboard/dashboard/src/views/network/NetworkServers.js new file mode 100644 index 000000000..1f0202c92 --- /dev/null +++ b/react/dashboard/dashboard/src/views/network/NetworkServers.js @@ -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 + } + + return ( + + + setSelectedServer(index)}/> + + + {data?.servers.length && } + {data?.servers.length && } + + + ) +}; + +export default NetworkServers \ No newline at end of file diff --git a/react/dashboard/dashboard/src/views/network/NetworkSessions.js b/react/dashboard/dashboard/src/views/network/NetworkSessions.js new file mode 100644 index 000000000..6405e5e69 --- /dev/null +++ b/react/dashboard/dashboard/src/views/network/NetworkSessions.js @@ -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 ( + +
+ + + + + + + + + +
+
+ ) +} + +export default NetworkSessions; \ No newline at end of file diff --git a/react/dashboard/dashboard/src/views/server/PlayerbaseOverview.js b/react/dashboard/dashboard/src/views/server/PlayerbaseOverview.js index 75c7b3ebb..6d5729c2f 100644 --- a/react/dashboard/dashboard/src/views/server/PlayerbaseOverview.js +++ b/react/dashboard/dashboard/src/views/server/PlayerbaseOverview.js @@ -20,10 +20,10 @@ const PlayerbaseOverview = () => {
- + - + diff --git a/react/dashboard/dashboard/src/views/server/ServerGeolocations.js b/react/dashboard/dashboard/src/views/server/ServerGeolocations.js index 48460c50e..8656bc643 100644 --- a/react/dashboard/dashboard/src/views/server/ServerGeolocations.js +++ b/react/dashboard/dashboard/src/views/server/ServerGeolocations.js @@ -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 ( - -
- - - {loadingError ? : } - {pingLoadingError ? : - } - - -
-
+ ) }; diff --git a/react/dashboard/dashboard/src/views/server/ServerJoinAddresses.js b/react/dashboard/dashboard/src/views/server/ServerJoinAddresses.js new file mode 100644 index 000000000..a94cd6a2f --- /dev/null +++ b/react/dashboard/dashboard/src/views/server/ServerJoinAddresses.js @@ -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 ( + + + + + + + + + ) +}; + +export default ServerJoinAddresses \ No newline at end of file diff --git a/react/dashboard/dashboard/src/views/server/ServerSessions.js b/react/dashboard/dashboard/src/views/server/ServerSessions.js index e12d1407b..35381019e 100644 --- a/react/dashboard/dashboard/src/views/server/ServerSessions.js +++ b/react/dashboard/dashboard/src/views/server/ServerSessions.js @@ -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 (
- + - +
diff --git a/react/dashboard/dashboard/yarn.lock b/react/dashboard/dashboard/yarn.lock index 214156142..500c0bc25 100644 --- a/react/dashboard/dashboard/yarn.lock +++ b/react/dashboard/dashboard/yarn.lock @@ -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"