mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-11-16 15:45:16 +01:00
Visualize join address / day in React server page
Join addresses were missing - Implemented new group visualizer that allows viewing group data as column/bar/pie/table - Implemented latest join address pie on server page (was missing) - Implemented join addresses per day graph on new Join addresses tab - Made playerbase overview use the group visualizer Affects issues: - Close #2362
This commit is contained in:
parent
2c233a0d21
commit
cb21c45ea9
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of Player Analytics (Plan).
|
||||
*
|
||||
* Plan is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Plan is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.domain;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a single join address - number pair.
|
||||
*
|
||||
* @author AuroraLS3
|
||||
*/
|
||||
public class JoinAddressCount implements Comparable<JoinAddressCount> {
|
||||
|
||||
private final int count;
|
||||
private String joinAddress;
|
||||
|
||||
public JoinAddressCount(Map.Entry<String, Integer> entry) {
|
||||
this(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
public JoinAddressCount(String joinAddress, int count) {
|
||||
this.joinAddress = joinAddress;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public String getJoinAddress() {
|
||||
return joinAddress;
|
||||
}
|
||||
|
||||
public void setJoinAddress(String joinAddress) {
|
||||
this.joinAddress = joinAddress;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull JoinAddressCount other) {
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(this.joinAddress, other.joinAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
JoinAddressCount that = (JoinAddressCount) o;
|
||||
return getCount() == that.getCount() && Objects.equals(getJoinAddress(), that.getJoinAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getJoinAddress(), getCount());
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* This file is part of Player Analytics (Plan).
|
||||
*
|
||||
* Plan is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Plan is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author AuroraLS3
|
||||
*/
|
||||
public class JoinAddressCounts implements DateHolder {
|
||||
|
||||
private final long date;
|
||||
private final List<JoinAddressCount> joinAddresses;
|
||||
|
||||
public JoinAddressCounts(long date, List<JoinAddressCount> joinAddresses) {
|
||||
this.date = date;
|
||||
this.joinAddresses = joinAddresses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public List<JoinAddressCount> getJoinAddresses() {
|
||||
return joinAddresses;
|
||||
}
|
||||
}
|
@ -17,6 +17,9 @@
|
||||
package com.djrapitops.plan.delivery.rendering.json.graphs;
|
||||
|
||||
import com.djrapitops.plan.delivery.domain.DateMap;
|
||||
import com.djrapitops.plan.delivery.domain.DateObj;
|
||||
import com.djrapitops.plan.delivery.domain.JoinAddressCount;
|
||||
import com.djrapitops.plan.delivery.domain.JoinAddressCounts;
|
||||
import com.djrapitops.plan.delivery.domain.mutators.MutatorFunctions;
|
||||
import com.djrapitops.plan.delivery.domain.mutators.PingMutator;
|
||||
import com.djrapitops.plan.delivery.domain.mutators.TPSMutator;
|
||||
@ -29,8 +32,6 @@ import com.djrapitops.plan.delivery.rendering.json.graphs.pie.Pie;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.pie.WorldPie;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.special.WorldMap;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.stack.StackGraph;
|
||||
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
|
||||
import com.djrapitops.plan.delivery.web.resolver.request.URIQuery;
|
||||
import com.djrapitops.plan.gathering.domain.FinishedSession;
|
||||
import com.djrapitops.plan.gathering.domain.Ping;
|
||||
import com.djrapitops.plan.gathering.domain.WorldTimes;
|
||||
@ -51,6 +52,7 @@ import com.djrapitops.plan.storage.database.queries.analysis.NetworkActivityInde
|
||||
import com.djrapitops.plan.storage.database.queries.analysis.PlayerCountQueries;
|
||||
import com.djrapitops.plan.storage.database.queries.objects.*;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.JoinAddressTable;
|
||||
import com.djrapitops.plan.utilities.comparators.DateHolderOldestComparator;
|
||||
import com.djrapitops.plan.utilities.java.Lists;
|
||||
import com.djrapitops.plan.utilities.java.Maps;
|
||||
import net.playeranalytics.plugin.scheduling.TimeAmount;
|
||||
@ -62,6 +64,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Creates Graph related Data JSON.
|
||||
@ -123,9 +126,7 @@ public class GraphJSONCreator {
|
||||
"}}";
|
||||
}
|
||||
|
||||
public Map<String, Object> optimizedPerformanceGraphJSON(ServerUUID serverUUID, URIQuery query) {
|
||||
// long after = getAfter(query); // TODO Implement if performance issues become apparent.
|
||||
|
||||
public Map<String, Object> optimizedPerformanceGraphJSON(ServerUUID serverUUID) {
|
||||
long now = System.currentTimeMillis();
|
||||
long twoMonthsAgo = now - TimeUnit.DAYS.toMillis(60);
|
||||
long monthAgo = now - TimeUnit.DAYS.toMillis(30);
|
||||
@ -187,16 +188,6 @@ public class GraphJSONCreator {
|
||||
.build();
|
||||
}
|
||||
|
||||
private long getAfter(URIQuery query) {
|
||||
try {
|
||||
return query.get("after")
|
||||
.map(Long::parseLong)
|
||||
.orElse(0L) - 500L; // Some headroom for out-of-sync clock.
|
||||
} catch (NumberFormatException badType) {
|
||||
throw new BadRequestException("'after': " + badType.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public String playersOnlineGraph(ServerUUID serverUUID) {
|
||||
Database db = dbSystem.getDatabase();
|
||||
long now = System.currentTimeMillis();
|
||||
@ -401,7 +392,7 @@ public class GraphJSONCreator {
|
||||
long now = System.currentTimeMillis();
|
||||
List<Ping> pings = db.query(PingQueries.fetchPingDataOfServer(now - TimeUnit.DAYS.toMillis(180L), now, serverUUID));
|
||||
|
||||
PingGraph pingGraph = graphs.line().pingGraph(new PingMutator(pings).mutateToByMinutePings().all());// TODO Optimize in query
|
||||
PingGraph pingGraph = graphs.line().pingGraph(new PingMutator(pings).mutateToByMinutePings().all());
|
||||
|
||||
return "{\"min_ping_series\":" + pingGraph.getMinGraph().toHighChartsSeries() +
|
||||
",\"avg_ping_series\":" + pingGraph.getAvgGraph().toHighChartsSeries() +
|
||||
@ -468,4 +459,29 @@ public class GraphJSONCreator {
|
||||
joinAddresses.put(locale.getString(GenericLang.UNKNOWN).toLowerCase(), unknown);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> joinAddressesByDay(ServerUUID serverUUID, long after, long before) {
|
||||
String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
|
||||
List<DateObj<Map<String, Integer>>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(serverUUID, config.getTimeZone().getOffset(System.currentTimeMillis()), after, before));
|
||||
|
||||
for (DateObj<Map<String, Integer>> addressesByDate : joinAddresses) {
|
||||
translateUnknown(addressesByDate.getValue());
|
||||
}
|
||||
|
||||
List<JoinAddressCounts> joinAddressCounts = joinAddresses.stream()
|
||||
.map(addressesOnDay -> new JoinAddressCounts(
|
||||
addressesOnDay.getDate(),
|
||||
addressesOnDay.getValue().entrySet()
|
||||
.stream()
|
||||
.map(JoinAddressCount::new)
|
||||
.sorted()
|
||||
.collect(Collectors.toList())))
|
||||
.sorted(new DateHolderOldestComparator())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Maps.builder(String.class, Object.class)
|
||||
.put("colors", pieColors)
|
||||
.put("join_addresses_by_date", joinAddressCounts)
|
||||
.build();
|
||||
}
|
||||
}
|
@ -51,7 +51,8 @@ public enum DataID {
|
||||
EXTENSION_NAV,
|
||||
EXTENSION_TABS,
|
||||
EXTENSION_JSON,
|
||||
LIST_SERVERS;
|
||||
LIST_SERVERS,
|
||||
JOIN_ADDRESSES_BY_DAY;
|
||||
|
||||
public String of(ServerUUID serverUUID) {
|
||||
return name() + '-' + serverUUID;
|
||||
|
@ -98,6 +98,7 @@ public class GraphsJSONResolver implements Resolver {
|
||||
@ExampleObject("punchCard"),
|
||||
@ExampleObject("serverPie"),
|
||||
@ExampleObject("joinAddressPie"),
|
||||
@ExampleObject("joinAddressByDay"),
|
||||
}),
|
||||
@Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for", examples = {
|
||||
@ExampleObject("Server 1"),
|
||||
@ -178,6 +179,8 @@ public class GraphsJSONResolver implements Resolver {
|
||||
return DataID.GRAPH_SERVER_PIE;
|
||||
case "joinAddressPie":
|
||||
return DataID.GRAPH_HOSTNAME_PIE;
|
||||
case "joinAddressByDay":
|
||||
return DataID.JOIN_ADDRESSES_BY_DAY;
|
||||
default:
|
||||
throw new BadRequestException("unknown 'type' parameter.");
|
||||
}
|
||||
@ -188,7 +191,7 @@ public class GraphsJSONResolver implements Resolver {
|
||||
case GRAPH_PERFORMANCE:
|
||||
return graphJSON.performanceGraphJSON(serverUUID);
|
||||
case GRAPH_OPTIMIZED_PERFORMANCE:
|
||||
return graphJSON.optimizedPerformanceGraphJSON(serverUUID, query);
|
||||
return graphJSON.optimizedPerformanceGraphJSON(serverUUID);
|
||||
case GRAPH_ONLINE:
|
||||
return graphJSON.playersOnlineGraph(serverUUID);
|
||||
case GRAPH_UNIQUE_NEW:
|
||||
@ -209,6 +212,15 @@ public class GraphsJSONResolver implements Resolver {
|
||||
return graphJSON.pingGraphsJSON(serverUUID);
|
||||
case GRAPH_PUNCHCARD:
|
||||
return graphJSON.punchCardJSONAsMap(serverUUID);
|
||||
case JOIN_ADDRESSES_BY_DAY:
|
||||
try {
|
||||
return graphJSON.joinAddressesByDay(serverUUID,
|
||||
query.get("after").map(Long::parseLong).orElse(0L),
|
||||
query.get("before").map(Long::parseLong).orElse(System.currentTimeMillis())
|
||||
);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BadRequestException("'after' or 'before' is not a epoch millisecond (number) " + e.getMessage());
|
||||
}
|
||||
default:
|
||||
return Collections.singletonMap("error", "Undefined ID: " + id.name());
|
||||
}
|
||||
|
@ -122,6 +122,7 @@ public enum HtmlLang implements Lang {
|
||||
TITLE_PLAYERBASE_DEVELOPMENT("html.label.playerbaseDevelopment", "Playerbase development"),
|
||||
TITLE_CURRENT_PLAYERBASE("html.label.currentPlayerbase", "Current Playerbase"),
|
||||
TITLE_JOIN_ADDRESSES("html.label.joinAddresses", "Join Addresses"),
|
||||
TITLE_LATEST_JOIN_ADDRESSES("html.label.latestJoinAddresses", "Latest Join Addresses"),
|
||||
COMPARING_60_DAYS("html.text.comparing30daysAgo", "Comparing 30d ago to Now"),
|
||||
TITLE_30_DAYS_AGO("html.label.thirtyDaysAgo", "30 days ago"),
|
||||
TITLE_NOW("html.label.now", "Now"),
|
||||
|
@ -16,19 +16,23 @@
|
||||
*/
|
||||
package com.djrapitops.plan.storage.database.queries.objects;
|
||||
|
||||
import com.djrapitops.plan.delivery.domain.DateObj;
|
||||
import com.djrapitops.plan.identification.ServerUUID;
|
||||
import com.djrapitops.plan.storage.database.queries.Query;
|
||||
import com.djrapitops.plan.storage.database.queries.QueryAllStatement;
|
||||
import com.djrapitops.plan.storage.database.queries.QueryStatement;
|
||||
import com.djrapitops.plan.storage.database.queries.RowExtractors;
|
||||
import com.djrapitops.plan.storage.database.sql.building.Sql;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.JoinAddressTable;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.ServerTable;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.SessionsTable;
|
||||
import org.apache.commons.text.TextStringBuilder;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
|
||||
|
||||
@ -110,4 +114,48 @@ public class JoinAddressQueries {
|
||||
|
||||
return db -> db.querySet(sql, RowExtractors.getInt(SessionsTable.USER_ID), joinAddresses.toArray());
|
||||
}
|
||||
|
||||
public static Query<List<DateObj<Map<String, Integer>>>> joinAddressesPerDay(ServerUUID serverUUID, long timezoneOffset, long after, long before) {
|
||||
return db -> {
|
||||
Sql sql = db.getSql();
|
||||
|
||||
String selectAddresses = SELECT +
|
||||
sql.dateToEpochSecond(sql.dateToDayStamp(sql.epochSecondToDate('(' + SessionsTable.SESSION_START + "+?)/1000"))) +
|
||||
"*1000 as date," +
|
||||
JoinAddressTable.JOIN_ADDRESS +
|
||||
", COUNT(1) as count" +
|
||||
FROM + SessionsTable.TABLE_NAME + " s" +
|
||||
LEFT_JOIN + JoinAddressTable.TABLE_NAME + " j on s." + SessionsTable.JOIN_ADDRESS_ID + "=j." + JoinAddressTable.ID +
|
||||
WHERE + SessionsTable.SERVER_ID + "=" + ServerTable.SELECT_SERVER_ID +
|
||||
AND + SessionsTable.SESSION_START + ">?" +
|
||||
AND + SessionsTable.SESSION_START + "<=?" +
|
||||
GROUP_BY + "date,j." + JoinAddressTable.ID;
|
||||
|
||||
return db.query(new QueryStatement<>(selectAddresses, 1000) {
|
||||
@Override
|
||||
public void prepare(PreparedStatement statement) throws SQLException {
|
||||
statement.setLong(1, timezoneOffset);
|
||||
statement.setString(2, serverUUID.toString());
|
||||
statement.setLong(3, after);
|
||||
statement.setLong(4, before);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DateObj<Map<String, Integer>>> processResults(ResultSet set) throws SQLException {
|
||||
Map<Long, Map<String, Integer>> addressesByDate = new HashMap<>();
|
||||
while (set.next()) {
|
||||
long date = set.getLong("date");
|
||||
String joinAddress = set.getString(JoinAddressTable.JOIN_ADDRESS);
|
||||
int count = set.getInt("count");
|
||||
Map<String, Integer> joinAddresses = addressesByDate.computeIfAbsent(date, k -> new TreeMap<>());
|
||||
joinAddresses.put(joinAddress, count);
|
||||
}
|
||||
|
||||
return addressesByDate.entrySet()
|
||||
.stream().map(entry -> new DateObj<>(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -175,6 +175,7 @@ class AccessControlTest {
|
||||
"/v1/graph?type=hourlyUniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + ",200",
|
||||
"/v1/graph?type=serverCalendar&server=" + TestConstants.SERVER_UUID_STRING + ",200",
|
||||
"/v1/graph?type=punchCard&server=" + TestConstants.SERVER_UUID_STRING + ",200",
|
||||
"/v1/graph?type=joinAddressByDay&server=" + TestConstants.SERVER_UUID_STRING + "&after=0&before=" + 123456L + ",200",
|
||||
"/v1/players?server=" + TestConstants.SERVER_UUID_STRING + ",200",
|
||||
"/v1/kills?server=" + TestConstants.SERVER_UUID_STRING + ",200",
|
||||
"/v1/pingTable?server=" + TestConstants.SERVER_UUID_STRING + ",200",
|
||||
@ -251,6 +252,7 @@ class AccessControlTest {
|
||||
"/v1/kills?server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/pingTable?server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/sessions?server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/graph?type=joinAddressByDay&server=" + TestConstants.SERVER_UUID_STRING + "&after=0&before=" + 123456L + ",403",
|
||||
"/network,403",
|
||||
"/v1/network/overview,403",
|
||||
"/v1/network/servers,403",
|
||||
@ -319,6 +321,7 @@ class AccessControlTest {
|
||||
"/v1/graph?type=hourlyUniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/graph?type=serverCalendar&server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/graph?type=punchCard&server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/graph?type=joinAddressByDay&server=" + TestConstants.SERVER_UUID_STRING + "&after=0&before=" + 123456L + ",403",
|
||||
"/v1/players?server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/kills?server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/pingTable?server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
@ -391,6 +394,7 @@ class AccessControlTest {
|
||||
"/v1/graph?type=hourlyUniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/graph?type=serverCalendar&server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/graph?type=punchCard&server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/graph?type=joinAddressByDay&server=" + TestConstants.SERVER_UUID_STRING + "&after=0&before=" + 123456L + ",403",
|
||||
"/v1/players?server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/kills?server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
"/v1/pingTable?server=" + TestConstants.SERVER_UUID_STRING + ",403",
|
||||
|
@ -32,6 +32,7 @@ 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"));
|
||||
@ -100,6 +101,7 @@ function App() {
|
||||
<Route path="sessions" element={<Lazy><ServerSessions/></Lazy>}/>
|
||||
<Route path="pvppve" element={<Lazy><ServerPvpPve/></Lazy>}/>
|
||||
<Route path="playerbase" element={<Lazy><PlayerbaseOverview/></Lazy>}/>
|
||||
<Route path="join-addresses" element={<Lazy><ServerJoinAddresses/></Lazy>}/>
|
||||
<Route path="retention" element={<></>}/>
|
||||
<Route path="players" element={<Lazy><ServerPlayers/></Lazy>}/>
|
||||
<Route path="geolocations" element={<Lazy><ServerGeolocations/></Lazy>}/>
|
||||
|
@ -7,8 +7,8 @@ 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 {t} = useTranslation();
|
||||
@ -26,7 +26,7 @@ const CurrentPlayerbaseCard = () => {
|
||||
<Fa icon={faUsers} className="col-amber"/> {t('html.label.currentPlayerbase')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<PlayerbasePie series={data.activity_pie_series}/>
|
||||
<GroupVisualizer groups={data.activity_pie_series} name={t('html.label.players')}/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useParams} from "react-router-dom";
|
||||
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";
|
||||
|
||||
const JoinAddressGraphCard = () => {
|
||||
const {t} = useTranslation();
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchJoinAddressByDay, [identifier]);
|
||||
|
||||
if (loadingError) return <ErrorViewCard error={loadingError}/>
|
||||
if (!data) return <CardLoader/>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black" style={{width: '100%'}}>
|
||||
<Fa icon={faChartColumn} className="col-amber"/> {t('html.label.joinAddresses')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<JoinAddressGraph id={'join-address-graph'} data={data?.join_addresses_by_date} colors={data?.colors}/>
|
||||
</Card>
|
||||
)
|
||||
};
|
||||
|
||||
export default JoinAddressGraphCard
|
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useParams} from "react-router-dom";
|
||||
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 = () => {
|
||||
const {t} = useTranslation();
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchJoinAddressPie, [identifier]);
|
||||
|
||||
if (loadingError) return <ErrorViewCard error={loadingError}/>
|
||||
if (!data) return <CardLoader/>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black" style={{width: '100%'}}>
|
||||
<Fa icon={faLocationArrow} className="col-amber"/> {t('html.label.latestJoinAddresses')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<GroupVisualizer groups={data.slices} colors={data.colors}/>
|
||||
</Card>
|
||||
)
|
||||
};
|
||||
|
||||
export default JoinAddressGroupCard
|
56
Plan/react/dashboard/src/components/graphs/GroupBarGraph.js
Normal file
56
Plan/react/dashboard/src/components/graphs/GroupBarGraph.js
Normal file
@ -0,0 +1,56 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
import Highcharts from "highcharts";
|
||||
import Accessibility from "highcharts/modules/accessibility";
|
||||
|
||||
const GroupBarGraph = ({id, groups, colors, horizontal, name}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const reduceColors = (colorsToReduce) => colorsToReduce.map(color => withReducedSaturation(color));
|
||||
|
||||
function getColors() {
|
||||
const actualColors = colors ? colors : groups.map(group => group.color);
|
||||
return nightModeEnabled ? reduceColors(actualColors) : actualColors;
|
||||
}
|
||||
|
||||
const bars = groups.map(group => group.y);
|
||||
const categories = groups.map(group => t(group.name));
|
||||
const barSeries = {
|
||||
name: name,
|
||||
colorByPoint: true,
|
||||
data: bars,
|
||||
colors: getColors()
|
||||
};
|
||||
|
||||
Accessibility(Highcharts);
|
||||
Highcharts.setOptions(graphTheming);
|
||||
Highcharts.chart(id, {
|
||||
chart: {type: horizontal ? 'bar' : 'column'},
|
||||
title: {text: ''},
|
||||
xAxis: {
|
||||
categories: categories,
|
||||
title: {text: ''}
|
||||
},
|
||||
yAxis: {
|
||||
min: 0,
|
||||
title: {text: '', align: 'high'},
|
||||
labels: {overflow: 'justify'}
|
||||
},
|
||||
legend: {enabled: false},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
dataLabels: {enabled: true}
|
||||
}
|
||||
},
|
||||
series: [barSeries]
|
||||
})
|
||||
}, [id, groups, colors, horizontal, name, graphTheming, nightModeEnabled, t]);
|
||||
|
||||
return (<div id={id} className="chart-area"/>);
|
||||
};
|
||||
|
||||
export default GroupBarGraph
|
@ -1,28 +1,35 @@
|
||||
import React, {useEffect} from "react";
|
||||
import Highcharts from 'highcharts';
|
||||
import React, {useEffect} from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import Accessibility from "highcharts/modules/accessibility";
|
||||
import Highcharts from "highcharts";
|
||||
|
||||
const PlayerbasePie = ({series}) => {
|
||||
const GroupPie = ({id, groups, colors, name}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const reduceColors = (slices) => slices.map(slice => {
|
||||
return {...slice, color: withReducedSaturation(slice.color)}
|
||||
});
|
||||
const reduceColors = (colorsToReduce) => colorsToReduce.map(color => withReducedSaturation(color));
|
||||
|
||||
function getColors() {
|
||||
const actualColors = colors ? colors : groups.map(group => group.color);
|
||||
return nightModeEnabled ? reduceColors(actualColors) : actualColors;
|
||||
}
|
||||
|
||||
const series = groups.map(group => {
|
||||
return {name: t(group.name), y: group.y}
|
||||
});
|
||||
const pieSeries = {
|
||||
name: t('html.label.players'),
|
||||
name: name,
|
||||
colorByPoint: true,
|
||||
data: nightModeEnabled ? reduceColors(series) : series
|
||||
colors: getColors(),
|
||||
data: series
|
||||
};
|
||||
|
||||
Accessibility(Highcharts);
|
||||
Highcharts.setOptions(graphTheming);
|
||||
Highcharts.chart('playerbase-pie', {
|
||||
Highcharts.chart(id, {
|
||||
chart: {
|
||||
backgroundColor: 'transparent',
|
||||
plotBorderWidth: null,
|
||||
@ -40,11 +47,16 @@ const PlayerbasePie = ({series}) => {
|
||||
showInLegend: true
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
formatter: function () {
|
||||
return '<b>' + this.point.name + ':</b> ' + this.y;
|
||||
}
|
||||
},
|
||||
series: [pieSeries]
|
||||
});
|
||||
}, [series, graphTheming, nightModeEnabled, t]);
|
||||
}, [id, colors, groups, name, graphTheming, nightModeEnabled, t]);
|
||||
|
||||
return (<div className="chart-area" id="playerbase-pie"/>);
|
||||
}
|
||||
return (<div className="chart-area" id={id}/>);
|
||||
};
|
||||
|
||||
export default PlayerbasePie;
|
||||
export default GroupPie;
|
@ -0,0 +1,63 @@
|
||||
import React, {useState} from 'react';
|
||||
import GroupTable from "../table/GroupTable";
|
||||
import GroupPie from "./GroupPie";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faBarChart, faChartColumn, faPieChart, faTable} from "@fortawesome/free-solid-svg-icons";
|
||||
import {Col, Row} from "react-bootstrap-v5";
|
||||
import GroupBarGraph from "./GroupBarGraph";
|
||||
|
||||
const options = {
|
||||
BAR: 'bar',
|
||||
COLUMN: 'column',
|
||||
PIE: 'pie',
|
||||
TABLE: 'table'
|
||||
}
|
||||
|
||||
const Visualizer = ({option, groups, colors, name}) => {
|
||||
switch (option) {
|
||||
case options.TABLE:
|
||||
return <GroupTable groups={groups} colors={colors}/>
|
||||
case options.PIE:
|
||||
return <GroupPie id={'group-pie-' + new Date()} groups={groups} colors={colors} name={name}/>
|
||||
case options.BAR:
|
||||
return <GroupBarGraph id={'group-bar-' + new Date()} groups={groups} colors={colors} name={name}
|
||||
horizontal/>;
|
||||
case options.COLUMN:
|
||||
default:
|
||||
return <GroupBarGraph id={'group-bar-' + new Date()} groups={groups} colors={colors} name={name}/>;
|
||||
}
|
||||
}
|
||||
|
||||
const VisualizerSelector = ({onClick, icon}) => {
|
||||
return (
|
||||
<button className="btn float-end" onClick={onClick}>
|
||||
<FontAwesomeIcon icon={icon} className="col-gray"/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const GroupVisualizer = ({groups, colors, name, horizontal}) => {
|
||||
const [visualization, setVisualization] = useState(groups.length > 1 ? options.COLUMN : options.TABLE);
|
||||
|
||||
const selectorFloatStyle = {
|
||||
height: "0",
|
||||
zIndex: 100,
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
right: "0",
|
||||
top: "0.5rem"
|
||||
};
|
||||
return <Row>
|
||||
<Col md={12} style={selectorFloatStyle}>
|
||||
<VisualizerSelector icon={faPieChart} onClick={() => setVisualization(options.PIE)}/>
|
||||
<VisualizerSelector icon={faTable} onClick={() => setVisualization(options.TABLE)}/>
|
||||
<VisualizerSelector icon={horizontal ? faBarChart : faChartColumn}
|
||||
onClick={() => setVisualization(horizontal ? options.BAR : options.COLUMN)}/>
|
||||
</Col>
|
||||
<Col md={12}>
|
||||
<Visualizer option={visualization} groups={groups} colors={colors} name={name}/>
|
||||
</Col>
|
||||
</Row>
|
||||
};
|
||||
|
||||
export default GroupVisualizer
|
@ -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}) => {
|
||||
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: 'normal',
|
||||
lineWidth: 1
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
enabled: true
|
||||
},
|
||||
series: series
|
||||
})
|
||||
}, [data, colors, graphTheming, id, t, nightModeEnabled])
|
||||
|
||||
return (
|
||||
<div className="chart-area" style={{height: "450px"}} id={id}>
|
||||
<span className="loader"/>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default JoinAddressGraph
|
@ -9,6 +9,7 @@ import {useTranslation} from "react-i18next";
|
||||
const LineGraph = ({id, series}) => {
|
||||
const {t} = useTranslation()
|
||||
const {graphTheming, nightModeEnabled} = useTheme();
|
||||
console.log(series)
|
||||
|
||||
useEffect(() => {
|
||||
NoDataDisplay(Highcharts);
|
||||
|
@ -7,6 +7,7 @@ import {withReducedSaturation} from "../../util/colors";
|
||||
import Accessibility from "highcharts/modules/accessibility";
|
||||
|
||||
const PlayerbaseGraph = ({data}) => {
|
||||
console.log(data);
|
||||
const {t} = useTranslation()
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
|
44
Plan/react/dashboard/src/components/table/GroupTable.js
Normal file
44
Plan/react/dashboard/src/components/table/GroupTable.js
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
|
||||
const GroupRow = ({group, color}) => {
|
||||
return (
|
||||
<tr>
|
||||
<td style={{color}}>{group.name}</td>
|
||||
<td>{group.y}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const GroupTable = ({groups, colors}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled} = useTheme();
|
||||
|
||||
function getColor(i) {
|
||||
if (groups[i].color) {
|
||||
return nightModeEnabled ? withReducedSaturation(groups[i].color) : groups[i].color;
|
||||
}
|
||||
return nightModeEnabled ? withReducedSaturation(colors[i]) : colors[i];
|
||||
}
|
||||
|
||||
return (
|
||||
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
|
||||
<tbody>
|
||||
{groups.length ? groups.map((group, i) =>
|
||||
<GroupRow key={i}
|
||||
group={group}
|
||||
color={getColor(i)}/>) :
|
||||
<tr>
|
||||
<td>{t('generic.noData')}</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
};
|
||||
|
||||
export default GroupTable
|
@ -114,3 +114,13 @@ 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 = `/v1/graph?type=joinAddressPie&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchJoinAddressByDay = async (timestamp, identifier) => {
|
||||
const url = `/v1/graph?type=joinAddressByDay&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
faCubes,
|
||||
faGlobe,
|
||||
faInfoCircle,
|
||||
faLocationArrow,
|
||||
faSearch,
|
||||
faUserGroup,
|
||||
faUsers
|
||||
@ -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"},
|
||||
|
21
Plan/react/dashboard/src/views/server/ServerJoinAddresses.js
Normal file
21
Plan/react/dashboard/src/views/server/ServerJoinAddresses.js
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import {Col, Row} from "react-bootstrap-v5";
|
||||
import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddressGroupCard";
|
||||
import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard";
|
||||
|
||||
const ServerJoinAddresses = () => {
|
||||
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col lg={8}>
|
||||
<JoinAddressGraphCard/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<JoinAddressGroupCard/>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
};
|
||||
|
||||
export default ServerJoinAddresses
|
Loading…
Reference in New Issue
Block a user