Implemented network page join address tab

- Made it possible to toggle stack/side-by-side join addresses
This commit is contained in:
Aurora Lahtela 2022-09-10 10:45:03 +03:00
parent ab2dfbbbcf
commit 7a51633690
15 changed files with 147 additions and 35 deletions

View File

@ -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());
}

View File

@ -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());
}

View File

@ -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"),

View File

@ -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());
}
});
};
}
}

View File

@ -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>}/>

View File

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

View File

@ -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>
)
};

View File

@ -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]);

View File

@ -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}>

View File

@ -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 (
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
<tbody>
{groups.length ? groups.map((group, i) =>
<GroupRow key={i}
group={group}
color={getColor(i)}/>) :
<tr>
<td>{t('generic.noData')}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>}
</tbody>
</table>
<Scrollable>
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
<tbody>
{groups.length ? groups.map((group, i) =>
<GroupRow key={i}
group={group}
color={getColor(i)}/>) :
<tr>
<td>{t('generic.noData')}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>}
</tbody>
</table>
</Scrollable>
)
};

View File

@ -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());
}
};

View File

@ -117,11 +117,13 @@ export const fetchPingGraph = async (timestamp, identifier) => {
}
export const fetchJoinAddressPie = async (timestamp, identifier) => {
const url = `/v1/graph?type=joinAddressPie&server=${identifier}&timestamp=${timestamp}`;
const url = identifier ? `/v1/graph?type=joinAddressPie&server=${identifier}&timestamp=${timestamp}` :
`/v1/graph?type=joinAddressPie&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchJoinAddressByDay = async (timestamp, identifier) => {
const url = `/v1/graph?type=joinAddressByDay&server=${identifier}&timestamp=${timestamp}`;
const url = identifier ? `/v1/graph?type=joinAddressByDay&server=${identifier}&timestamp=${timestamp}` :
`/v1/graph?type=joinAddressByDay&timestamp=${timestamp}`;
return doGetRequest(url);
}

View File

@ -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"},

View File

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

View File

@ -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>
)