mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-12-27 09:00:28 +08:00
Implemented network page join address tab
- Made it possible to toggle stack/side-by-side join addresses
This commit is contained in:
parent
ab2dfbbbcf
commit
7a51633690
@ -464,6 +464,17 @@ public class GraphJSONCreator {
|
||||
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));
|
||||
|
||||
return mapToJson(pieColors, joinAddresses);
|
||||
}
|
||||
|
||||
public Map<String, Object> joinAddressesByDay(long after, long before) {
|
||||
String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
|
||||
List<DateObj<Map<String, Integer>>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(config.getTimeZone().getOffset(System.currentTimeMillis()), after, before));
|
||||
|
||||
return mapToJson(pieColors, joinAddresses);
|
||||
}
|
||||
|
||||
private Map<String, Object> mapToJson(String[] pieColors, List<DateObj<Map<String, Integer>>> joinAddresses) {
|
||||
for (DateObj<Map<String, Integer>> addressesByDate : joinAddresses) {
|
||||
translateUnknown(addressesByDate.getValue());
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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"),
|
||||
|
@ -158,4 +158,46 @@ public class JoinAddressQueries {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public static Query<List<DateObj<Map<String, Integer>>>> 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<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());
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
<Route path="overview" element={<Lazy><NetworkOverview/></Lazy>}/>
|
||||
<Route path="serversOverview" element={<Lazy><NetworkServers/></Lazy>}/>
|
||||
<Route path="sessions" element={<Lazy><NetworkSessions/></Lazy>}/>
|
||||
<Route path="join-addresses" element={<Lazy><NetworkJoinAddresses/></Lazy>}/>
|
||||
<Route path="players" element={<Lazy><AllPlayers/></Lazy>}/>
|
||||
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
|
||||
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
|
||||
|
20
Plan/react/dashboard/src/components/Toggle.js
Normal file
20
Plan/react/dashboard/src/components/Toggle.js
Normal file
@ -0,0 +1,20 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
const Toggle = ({children, value, onValueChange, color}) => {
|
||||
const [renderTime] = useState(new Date().getTime());
|
||||
const id = 'checkbox-' + renderTime;
|
||||
|
||||
const handleChange = () => {
|
||||
onValueChange(!value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-check form-switch">
|
||||
<input id={id} type={"checkbox"} className={"form-check-input bg-" + color} role="switch"
|
||||
onChange={handleChange} checked={value}/>
|
||||
<label className="form-check-label" htmlFor={id}>{children}</label>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default Toggle
|
@ -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 <ErrorViewCard error={loadingError}/>
|
||||
if (!data) return <CardLoader/>;
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<h6 className="col-black" style={{width: '100%'}}>
|
||||
<Fa icon={faChartColumn} className="col-amber"/> {t('html.label.joinAddresses')}
|
||||
</h6>
|
||||
<Toggle value={stack} onValueChange={setStack} color={'amber'}>{t('html.label.stacked')}</Toggle>
|
||||
</Card.Header>
|
||||
<JoinAddressGraph id={'join-address-graph'} data={data?.join_addresses_by_date} colors={data?.colors}/>
|
||||
<JoinAddressGraph id={'join-address-graph'} data={data?.join_addresses_by_date} colors={data?.colors}
|
||||
stack={stack}/>
|
||||
</Card>
|
||||
)
|
||||
};
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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 (
|
||||
<div className="chart-area" style={{height: "450px"}} id={id}>
|
||||
|
@ -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,6 +25,7 @@ const GroupTable = ({groups, colors}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Scrollable>
|
||||
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
|
||||
<tbody>
|
||||
{groups.length ? groups.map((group, i) =>
|
||||
@ -38,6 +40,7 @@ const GroupTable = ({groups, colors}) => {
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</Scrollable>
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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"},
|
||||
|
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import {Col, Row} from "react-bootstrap-v5";
|
||||
import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddressGroupCard";
|
||||
import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard";
|
||||
|
||||
const NetworkJoinAddresses = () => {
|
||||
return (
|
||||
<Row>
|
||||
<Col lg={8}>
|
||||
<JoinAddressGraphCard identifier={undefined}/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<JoinAddressGroupCard identifier={undefined}/>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
};
|
||||
|
||||
export default NetworkJoinAddresses
|
@ -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 (
|
||||
<Row>
|
||||
<Col lg={8}>
|
||||
<JoinAddressGraphCard/>
|
||||
<JoinAddressGraphCard identifier={identifier}/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<JoinAddressGroupCard/>
|
||||
<JoinAddressGroupCard identifier={identifier}/>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user