diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java index d11dedcaf..a2757d712 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java @@ -464,6 +464,17 @@ public class GraphJSONCreator { String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE); List>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(serverUUID, config.getTimeZone().getOffset(System.currentTimeMillis()), after, before)); + return mapToJson(pieColors, joinAddresses); + } + + public Map joinAddressesByDay(long after, long before) { + String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE); + List>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(config.getTimeZone().getOffset(System.currentTimeMillis()), after, before)); + + return mapToJson(pieColors, joinAddresses); + } + + private Map mapToJson(String[] pieColors, List>> joinAddresses) { for (DateObj> addressesByDate : joinAddresses) { translateUnknown(addressesByDate.getValue()); } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java index 10f54feb5..052d5b9e3 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java @@ -145,7 +145,7 @@ public class GraphsJSONResolver implements Resolver { } else { // Assume network storedJSON = jsonResolverService.resolve( - timestamp, dataID, () -> generateGraphDataJSONOfType(dataID) + timestamp, dataID, () -> generateGraphDataJSONOfType(dataID, request.getQuery()) ); } return storedJSON; @@ -226,7 +226,7 @@ public class GraphsJSONResolver implements Resolver { } } - private Object generateGraphDataJSONOfType(DataID id) { + private Object generateGraphDataJSONOfType(DataID id, URIQuery query) { switch (id) { case GRAPH_ACTIVITY: return graphJSON.activityGraphsJSONAsMap(); @@ -240,6 +240,15 @@ public class GraphsJSONResolver implements Resolver { return graphJSON.playerHostnamePieJSONAsMap(); case GRAPH_WORLD_MAP: return graphJSON.geolocationGraphsJSONAsMap(); + case JOIN_ADDRESSES_BY_DAY: + try { + return graphJSON.joinAddressesByDay( + 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()); } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java index 818bb4ace..3d9fa5606 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java @@ -251,6 +251,7 @@ public enum HtmlLang implements Lang { LABEL_TOTAL("html.label.total", "Total"), LABEL_ALPHABETICAL("html.label.alphabetical", "Alphabetical"), LABEL_SORT_BY("html.label.sortBy", "Sort By"), + LABEL_STACKED("html.label.stacked", "Stacked"), LOGIN_LOGIN("html.login.login", "Login"), LOGIN_LOGOUT("html.login.logout", "Logout"), diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/JoinAddressQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/JoinAddressQueries.java index 5b3251498..0fcb80fa1 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/JoinAddressQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/JoinAddressQueries.java @@ -158,4 +158,46 @@ public class JoinAddressQueries { }); }; } + + public static Query>>> joinAddressesPerDay(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.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.setLong(2, after); + statement.setLong(3, before); + } + + @Override + public List>> processResults(ResultSet set) throws SQLException { + Map> addressesByDate = new HashMap<>(); + while (set.next()) { + long date = set.getLong("date"); + String joinAddress = set.getString(JoinAddressTable.JOIN_ADDRESS); + int count = set.getInt("count"); + Map 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()); + } + }); + }; + } } diff --git a/Plan/react/dashboard/src/App.js b/Plan/react/dashboard/src/App.js index 9de041e80..987399c2e 100644 --- a/Plan/react/dashboard/src/App.js +++ b/Plan/react/dashboard/src/App.js @@ -38,6 +38,7 @@ 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 PlayersPage = React.lazy(() => import("./views/layout/PlayersPage")); const AllPlayers = React.lazy(() => import("./views/players/AllPlayers")); @@ -121,6 +122,7 @@ function App() { }/> }/> }/> + }/> }/> }/> }/> diff --git a/Plan/react/dashboard/src/components/Toggle.js b/Plan/react/dashboard/src/components/Toggle.js new file mode 100644 index 000000000..e9239c029 --- /dev/null +++ b/Plan/react/dashboard/src/components/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/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.js b/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.js index 743fdee11..63471bc9c 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.js +++ b/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.js @@ -1,6 +1,5 @@ -import React from 'react'; +import React, {useState} 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"; @@ -9,24 +8,28 @@ 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 "../../../Toggle"; -const JoinAddressGraphCard = () => { +const JoinAddressGraphCard = ({identifier}) => { const {t} = useTranslation(); - const {identifier} = useParams(); + 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')}
- +
) }; diff --git a/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.js b/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.js index d8f040314..952758b01 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.js +++ b/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.js @@ -1,6 +1,5 @@ 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"; @@ -10,9 +9,8 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; import {faLocationArrow} from "@fortawesome/free-solid-svg-icons"; import GroupVisualizer from "../../../graphs/GroupVisualizer"; -const JoinAddressGroupCard = () => { +const JoinAddressGroupCard = ({identifier}) => { const {t} = useTranslation(); - const {identifier} = useParams(); const {data, loadingError} = useDataRequest(fetchJoinAddressPie, [identifier]); diff --git a/Plan/react/dashboard/src/components/graphs/JoinAddressGraph.js b/Plan/react/dashboard/src/components/graphs/JoinAddressGraph.js index 872b915ae..079b7c251 100644 --- a/Plan/react/dashboard/src/components/graphs/JoinAddressGraph.js +++ b/Plan/react/dashboard/src/components/graphs/JoinAddressGraph.js @@ -7,7 +7,7 @@ import Highcharts from "highcharts/highstock"; import Accessibility from "highcharts/modules/accessibility"; import {linegraphButtons} from "../../util/graphs"; -const JoinAddressGraph = ({id, data, colors}) => { +const JoinAddressGraph = ({id, data, colors, stack}) => { const {t} = useTranslation() const {nightModeEnabled, graphTheming} = useTheme(); @@ -24,7 +24,7 @@ const JoinAddressGraph = ({id, data, colors}) => { const valuesByAddress = {}; const dates = [] - for (const point of data) { + for (const point of data || []) { dates.push(point.date); for (const address of point.joinAddresses) { if (!valuesByAddress[address.joinAddress]) valuesByAddress[address.joinAddress] = []; @@ -61,7 +61,7 @@ const JoinAddressGraph = ({id, data, colors}) => { title: {text: ''}, plotOptions: { column: { - stacking: 'normal', + stacking: stack ? 'normal' : undefined, lineWidth: 1 } }, @@ -70,7 +70,7 @@ const JoinAddressGraph = ({id, data, colors}) => { }, series: series }) - }, [data, colors, graphTheming, id, t, nightModeEnabled]) + }, [data, colors, graphTheming, id, t, nightModeEnabled, stack]) return (
diff --git a/Plan/react/dashboard/src/components/table/GroupTable.js b/Plan/react/dashboard/src/components/table/GroupTable.js index 621429fa6..686c95856 100644 --- a/Plan/react/dashboard/src/components/table/GroupTable.js +++ b/Plan/react/dashboard/src/components/table/GroupTable.js @@ -2,6 +2,7 @@ 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 ( @@ -24,20 +25,22 @@ const GroupTable = ({groups, colors}) => { } return ( - - - {groups.length ? groups.map((group, i) => - ) : - - - - - - } - -
{t('generic.noData')}---
+ + + + {groups.length ? groups.map((group, i) => + ) : + + + + + + } + +
{t('generic.noData')}---
+
) }; diff --git a/Plan/react/dashboard/src/hooks/dataFetchHook.js b/Plan/react/dashboard/src/hooks/dataFetchHook.js index e15ca6129..b0211014b 100644 --- a/Plan/react/dashboard/src/hooks/dataFetchHook.js +++ b/Plan/react/dashboard/src/hooks/dataFetchHook.js @@ -51,7 +51,7 @@ export const useDataRequest = (fetchMethod, parameters) => { console.warn(error); datastore.finishUpdate(fetchMethod) setLoadingError(error); - finishUpdate(new Date().getTime(), "Error: " + error, datastore.isSomethingUpdating()); + finishUpdate(0, "Error: " + error.message, datastore.isSomethingUpdating()); } }; diff --git a/Plan/react/dashboard/src/service/serverService.js b/Plan/react/dashboard/src/service/serverService.js index 07e092068..e4b483402 100644 --- a/Plan/react/dashboard/src/service/serverService.js +++ b/Plan/react/dashboard/src/service/serverService.js @@ -117,11 +117,13 @@ export const fetchPingGraph = async (timestamp, identifier) => { } export const fetchJoinAddressPie = async (timestamp, identifier) => { - const url = `/v1/graph?type=joinAddressPie&server=${identifier}×tamp=${timestamp}`; + 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 = `/v1/graph?type=joinAddressByDay&server=${identifier}×tamp=${timestamp}`; + const url = identifier ? `/v1/graph?type=joinAddressByDay&server=${identifier}×tamp=${timestamp}` : + `/v1/graph?type=joinAddressByDay×tamp=${timestamp}`; return doGetRequest(url); } diff --git a/Plan/react/dashboard/src/views/layout/NetworkPage.js b/Plan/react/dashboard/src/views/layout/NetworkPage.js index 8e51e9647..97401ac8b 100644 --- a/Plan/react/dashboard/src/views/layout/NetworkPage.js +++ b/Plan/react/dashboard/src/views/layout/NetworkPage.js @@ -8,6 +8,7 @@ import { faCubes, faGlobe, faInfoCircle, + faLocationArrow, faNetworkWired, faSearch, faServer, @@ -73,6 +74,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"}, diff --git a/Plan/react/dashboard/src/views/network/NetworkJoinAddresses.js b/Plan/react/dashboard/src/views/network/NetworkJoinAddresses.js new file mode 100644 index 000000000..358917d2b --- /dev/null +++ b/Plan/react/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/Plan/react/dashboard/src/views/server/ServerJoinAddresses.js b/Plan/react/dashboard/src/views/server/ServerJoinAddresses.js index 024300ab8..a94cd6a2f 100644 --- a/Plan/react/dashboard/src/views/server/ServerJoinAddresses.js +++ b/Plan/react/dashboard/src/views/server/ServerJoinAddresses.js @@ -2,17 +2,17 @@ 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 ( - + - + )