Compare commits

...

14 Commits

Author SHA1 Message Date
Kevin Belisle 5b7885368f Improve error handling - just display nothing 2021-09-03 11:56:02 -04:00
Kevin Belisle d224ac81d8 Fix react build warnings 2021-09-03 11:49:42 -04:00
Kevin Belisle c3286bfa7f Sort player stats by rank, then value 2021-09-03 11:47:40 -04:00
Kevin Belisle 7c637f181e Fix aggregatesShowcase error handling 2021-09-03 11:45:23 -04:00
Kevin Belisle 34e5e207d9 Make the aggregate bar charts more saturated 2021-09-03 11:44:26 -04:00
Kevin Belisle 8e27e95fd9 Fix aggregates - again. 2021-09-03 11:10:26 -04:00
Kevin Belisle 6b3f87c401 Fix aggregates update SQL 2021-09-02 22:29:29 -04:00
Kevin Belisle 24d33afd59 Add database schema migration 2021-09-02 00:54:10 -04:00
Kevin Belisle a86453f497 Fix aggregates calculations 2021-09-02 00:25:24 -04:00
Kevin Belisle 959ac79fbd Fix regex for Safari in other file 2021-09-01 21:10:56 -04:00
Kevin Belisle 7c359aa390 Remove apparently useless look behind (fix Safari) 2021-09-01 12:09:35 -04:00
Kevin Belisle 7f9f9a01c4 Fix player name refresh check 2021-09-01 11:55:22 -04:00
Kevin Belisle e4ecd5d1b1 Fix aggregats URL 2021-07-21 15:54:01 -04:00
Kevin Belisle 2ba00ab2c6 Filter stats where value = 0 2021-07-21 15:53:40 -04:00
9 changed files with 170 additions and 147 deletions

View File

@ -4,6 +4,7 @@ import com.natpryce.konfig.*
import java.io.FileInputStream import java.io.FileInputStream
import java.util.* import java.util.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.*
import org.h2.tools.Server import org.h2.tools.Server
import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
@ -11,6 +12,7 @@ import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import xyz.etztech.stonks.api.initApiServer import xyz.etztech.stonks.api.initApiServer
import xyz.etztech.stonks.dsl.AggregateStatistics import xyz.etztech.stonks.dsl.AggregateStatistics
import xyz.etztech.stonks.dsl.KeyValue
import xyz.etztech.stonks.dsl.LiveStatistics import xyz.etztech.stonks.dsl.LiveStatistics
import xyz.etztech.stonks.dsl.Players import xyz.etztech.stonks.dsl.Players
import xyz.etztech.stonks.dsl.Statistics import xyz.etztech.stonks.dsl.Statistics
@ -91,6 +93,7 @@ fun initH2Server(
SchemaUtils.create(LiveStatistics) SchemaUtils.create(LiveStatistics)
SchemaUtils.create(AggregateStatistics) SchemaUtils.create(AggregateStatistics)
SchemaUtils.create(Players) SchemaUtils.create(Players)
SchemaUtils.create(KeyValue)
// Create indexes with explicit SQL because I can't figure out how to do it with exposed // Create indexes with explicit SQL because I can't figure out how to do it with exposed
// Wrap it in a try block because it throws an exception if index already exists // Wrap it in a try block because it throws an exception if index already exists
@ -113,6 +116,32 @@ fun initH2Server(
TransactionManager.current() TransactionManager.current()
.exec("CREATE INDEX idx_type_name ON LiveStatistics (\"Type\", \"Name\")") .exec("CREATE INDEX idx_type_name ON LiveStatistics (\"Type\", \"Name\")")
} catch (e: ExposedSQLException) {} } catch (e: ExposedSQLException) {}
val schemaVersions =
KeyValue.slice(KeyValue.value).select { KeyValue.key.eq("SchemaVersion") }.map {
it[KeyValue.value]
}
val schemaVersion = if (schemaVersions.count() > 0) schemaVersions.single() else 0
println("Database schemaVersion = ${schemaVersion}")
if (schemaVersion < 1) {
println("Migrating database to schemaVersion 1.")
TransactionManager.current().exec("TRUNCATE TABLE AGGREGATESTATISTICS")
KeyValue.insert {
it[KeyValue.key] = "SchemaVersion"
it[KeyValue.value] = 1
}
}
if (schemaVersion < 2) {
println("Migrating database to schemaVersion 2.")
TransactionManager.current().exec("TRUNCATE TABLE AGGREGATESTATISTICS")
KeyValue.update({ KeyValue.key eq "SchemaVersion" }) { it[KeyValue.value] = 2 }
}
} }
return database return database

View File

@ -41,3 +41,10 @@ object Players : Table() {
override val primaryKey = PrimaryKey(id, name = "PK_id") override val primaryKey = PrimaryKey(id, name = "PK_id")
} }
object KeyValue : Table() {
val key: Column<String> = varchar("Key", 150)
val value: Column<Int> = integer("Value")
override val primaryKey = PrimaryKey(key, name = "PK_key")
}

View File

@ -65,8 +65,6 @@ object StatisticsImporter {
} }
} }
transaction(database) { transaction(database) {
addLogger(StdOutSqlLogger)
statsFile?.stats?.forEach { type, stats -> statsFile?.stats?.forEach { type, stats ->
stats.forEach { name, value -> stats.forEach { name, value ->
if (playerStats.get(type)?.get(name) != value) { if (playerStats.get(type)?.get(name) != value) {
@ -134,29 +132,53 @@ object StatisticsImporter {
SET @Timestamp = CURRENT_TIMESTAMP; SET @Timestamp = CURRENT_TIMESTAMP;
INSERT INTO AGGREGATESTATISTICS ("Type", "Name", "Timestamp", "Value") INSERT INTO AGGREGATESTATISTICS ("Type", "Name", "Timestamp", "Value")
SELECT Live."Type", SELECT LiveMax."Type",
'', '',
@Timestamp AS "Timestamp", @Timestamp AS "Timestamp",
sum(Live."Value") SUM(LiveMax."Value")
FROM LIVESTATISTICS as Live FROM (
LEFT JOIN AGGREGATESTATISTICS as Agg SELECT Live."Type",
ON Live."Type" = Agg."Type" Live."Name",
WHERE array_contains(array['minecraft:mined'], Live."Type") MAX(Live."Value") AS "Value"
GROUP BY Live."Type" FROM livestatistics as Live
HAVING sum(Live."Value") <> max(Agg."Value") OR max(Agg."Value") IS NULL; WHERE array_contains(array['minecraft:mined'], Live."Type")
GROUP BY Live."Type", Live."Name", Live."PlayerId"
) as LiveMax
LEFT JOIN (
SELECT AGGREGATESTATISTICS."Type",
AGGREGATESTATISTICS."Name",
MAX(AGGREGATESTATISTICS."Value") as "Value"
FROM AGGREGATESTATISTICS
GROUP BY AGGREGATESTATISTICS."Type", AGGREGATESTATISTICS."Name"
) as Agg
ON LiveMax."Type" = Agg."Type"
GROUP BY LiveMax."Type"
HAVING sum(LiveMax."Value") <> max(Agg."Value") OR max(Agg."Value") IS NULL;
INSERT INTO AGGREGATESTATISTICS ("Type", "Name", "Timestamp", "Value") INSERT INTO AGGREGATESTATISTICS ("Type", "Name", "Timestamp", "Value")
SELECT Live."Type", SELECT LiveMax."Type",
Live."Name", LiveMax."Name",
@Timestamp AS "Timestamp", @Timestamp AS "Timestamp",
Sum(Live."Value") SUM(LiveMax."Value")
FROM livestatistics as Live FROM (
LEFT JOIN AGGREGATESTATISTICS as Agg SELECT Live."Type",
ON Live."Type" = Agg."Type" AND Live."Name" = Agg."Name" Live."Name",
WHERE array_contains(array['minecraft:animals_bred', 'minecraft:play_one_minute', 'minecraft:deaths', 'minecraft:player_kills', 'minecraft:aviate_one_cm', 'minecraft:boat_one_cm', 'minecraft:crouch_one_cm', 'minecraft:horse_one_cm', 'minecraft:minecart_one_cm', 'minecraft:sprint_one_cm', 'minecraft:strider_one_cm', 'minecraft:swim_one_cm', 'minecraft:walk_on_water_one_cm', 'minecraft:walk_one_cm', 'minecraft:walk_under_water_one_cm' ], Live."Name") MAX(Live."Value") AS "Value"
OR array_contains(array['minecraft:killed', 'minecraft:killed_by'], Live."Type") FROM livestatistics as Live
GROUP BY Live."Type", Live."Name" WHERE array_contains(array['minecraft:animals_bred', 'minecraft:play_one_minute', 'minecraft:deaths', 'minecraft:player_kills', 'minecraft:aviate_one_cm', 'minecraft:boat_one_cm', 'minecraft:crouch_one_cm', 'minecraft:horse_one_cm', 'minecraft:minecart_one_cm', 'minecraft:sprint_one_cm', 'minecraft:strider_one_cm', 'minecraft:swim_one_cm', 'minecraft:walk_on_water_one_cm', 'minecraft:walk_one_cm', 'minecraft:walk_under_water_one_cm' ], Live."Name")
HAVING sum(Live."Value") <> max(Agg."Value") OR max(Agg."Value") IS NULL; OR array_contains(array['minecraft:killed', 'minecraft:killed_by'], Live."Type")
GROUP BY Live."Type", Live."Name", Live."PlayerId"
) as LiveMax
LEFT JOIN (
SELECT AGGREGATESTATISTICS."Type",
AGGREGATESTATISTICS."Name",
MAX(AGGREGATESTATISTICS."Value") as "Value"
FROM AGGREGATESTATISTICS
GROUP BY AGGREGATESTATISTICS."Type", AGGREGATESTATISTICS."Name"
) as Agg
ON LiveMax."Type" = Agg."Type" AND LiveMax."Name" = Agg."Name"
GROUP BY LiveMax."Type", LiveMax."Name"
HAVING SUM(LiveMax."Value") <> MAX(Agg."Value") OR MAX(Agg."Value") IS NULL;
""".trimIndent() """.trimIndent()
) )
} }
@ -180,7 +202,7 @@ OR array_contains(array['minecraft:killed', 'minecraft:killed_by'], Live."Type")
.map { it[LiveStatistics.playerId] } .map { it[LiveStatistics.playerId] }
savedPlayers savedPlayers
.filter { Duration.between(Instant.now(), it.timestamp) > Duration.ofDays(1) } .filter { Duration.between(it.timestamp, Instant.now()) > Duration.ofDays(1) }
.forEach { player -> .forEach { player ->
runBlocking { runBlocking {
val name = getName(player.id) val name = getName(player.id)

View File

@ -2,59 +2,43 @@ import React from "react";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import axios from "axios"; import axios from "axios";
import { import { SimpleGrid, Stat, StatLabel, StatNumber } from "@chakra-ui/react";
AspectRatio,
Box,
Flex,
Heading,
HStack,
Icon,
SimpleGrid,
Stat,
StatLabel,
StatNumber,
StatHelpText,
StatArrow,
StatGroup,
Tooltip,
} from "@chakra-ui/react";
import { FaUserCircle, FaArrowAltCircleRight } from "react-icons/fa";
import { Link } from "react-router-dom";
import { AutoSizer, WindowScroller, List } from "react-virtualized";
import prettifyStatisticName from "./PrettifyStatisticName"; import prettifyStatisticName from "./PrettifyStatisticName";
import StackedBar from "./StackedBar"; import StackedBar from "./StackedBar";
const AggregatesShowcase = () => { const AggregatesShowcase = () => {
const aggregates = useQuery( const aggregates = useQuery(`aggregates`, async () => {
`aggregates`, const { data } = await axios.get(`/api/aggregates`);
async () => { return data;
const { data } = await axios.get(`http://localhost:7000/api/aggregates`); });
return data;
},
{
placeholderData: [],
}
);
const killedAggregates = aggregates.data const killedAggregates = aggregates.isSuccess
.filter((x) => x.type === "minecraft:killed") ? aggregates.data
.sort((a, b) => b.value - a.value); .filter((x) => x.type === "minecraft:killed")
const killedTotal = killedAggregates.reduce((acc, val) => acc + val.value, 0); .sort((a, b) => b.value - a.value)
: [];
// const killedTotal = killedAggregates.reduce((acc, val) => acc + val.value, 0);
const killedByAggregates = aggregates.data const killedByAggregates = aggregates.isSuccess
.filter((x) => x.type === "minecraft:killed_by") ? aggregates.data
.sort((a, b) => b.value - a.value); .filter((x) => x.type === "minecraft:killed_by")
const killedByTotal = killedByAggregates.reduce( .sort((a, b) => b.value - a.value)
(acc, val) => acc + val.value, : [];
0 // const killedByTotal = killedByAggregates.reduce(
); // (acc, val) => acc + val.value,
// 0
// );
const travelAggregates = aggregates.data const travelAggregates = aggregates.isSuccess
.filter((x) => x.type === "minecraft:custom" && x.name.endsWith("one_cm")) ? aggregates.data
.sort((a, b) => b.value - a.value); .filter(
const travelTotal = travelAggregates.reduce((acc, val) => acc + val.value, 0); (x) => x.type === "minecraft:custom" && x.name.endsWith("one_cm")
)
.sort((a, b) => b.value - a.value)
: [];
// const travelTotal = travelAggregates.reduce((acc, val) => acc + val.value, 0);
return aggregates.isFetched ? ( return aggregates.isSuccess ? (
<> <>
<StackedBar heading="Mobs Killed" aggregates={killedAggregates} /> <StackedBar heading="Mobs Killed" aggregates={killedAggregates} />
@ -73,18 +57,15 @@ const AggregatesShowcase = () => {
.map((x, i) => { .map((x, i) => {
var value = x.value; var value = x.value;
if (x.name === "minecraft:play_one_minute") { if (x.name === "minecraft:play_one_minute") {
value = x.value / 20 / 60; value = Math.floor(x.value / 20 / 60);
} }
return ( return (
<Stat key={i}> <Stat key={i}>
<StatLabel>{prettifyStatisticName(x.type, x.name)}</StatLabel> <StatLabel>{prettifyStatisticName(x.type, x.name)}</StatLabel>
<StatNumber>{`${x.value <StatNumber>{`${value
.toString() .toString()
.replace( .replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</StatNumber>
/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g,
","
)}`}</StatNumber>
</Stat> </Stat>
); );
})} })}

View File

@ -41,14 +41,17 @@ const App = () => {
<Statistic <Statistic
type={routeProps.match.params.type} type={routeProps.match.params.type}
name={routeProps.match.params.name} name={routeProps.match.params.name}
players={players} players={players.data}
/> />
)} )}
/> />
<Route <Route
path="/player/:id" path="/player/:id"
render={(routeProps) => ( render={(routeProps) => (
<Player playerId={routeProps.match.params.id} players={players} /> <Player
playerId={routeProps.match.params.id}
players={players.data}
/>
)} )}
/> />
<Route> <Route>

View File

@ -16,19 +16,13 @@ import { Link } from "react-router-dom";
import prettifyStatisticName from "./PrettifyStatisticName"; import prettifyStatisticName from "./PrettifyStatisticName";
const Player = ({ playerId, players }) => { const Player = ({ playerId, players }) => {
const playerStats = useQuery( const playerStats = useQuery(`player ${playerId}`, async () => {
`player ${playerId}`, const { data } = await axios.get(`/api/players/${playerId}`);
async () => { data.sort((a, b) => a.rank - b.rank || b.value - a.value);
const { data } = await axios.get(`/api/players/${playerId}`); return data.filter((x) => x.value > 0);
data.sort((a, b) => a.rank - b.rank); });
return data;
},
{
placeholderData: [],
}
);
const playerName = players.data.find((x) => x.id === playerId)?.name; const playerName = players?.find((x) => x.id === playerId)?.name;
return ( return (
<> <>
@ -60,19 +54,23 @@ const Player = ({ playerId, players }) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{playerStats.data.map((x, i) => { {playerStats.isSuccess ? (
return ( playerStats.data.map((x, i) => {
<Tr key={i}> return (
<Td> <Tr key={i}>
<Link to={`/statistic/${x.type}/${x.name}`}> <Td>
{prettifyStatisticName(x.type, x.name)} <Link to={`/statistic/${x.type}/${x.name}`}>
</Link> {prettifyStatisticName(x.type, x.name)}
</Td> </Link>
<Td isNumeric>{x.rank}</Td> </Td>
<Td isNumeric>{x.value}</Td> <Td isNumeric>{x.rank}</Td>
</Tr> <Td isNumeric>{x.value}</Td>
); </Tr>
})} );
})
) : (
<></>
)}
</Tbody> </Tbody>
</Table> </Table>
</> </>

View File

@ -1,21 +1,6 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import useHover from "./useHover"; import useHover from "./useHover";
import { import { Box, Heading, HStack, Tooltip } from "@chakra-ui/react";
AspectRatio,
Box,
Button,
Heading,
HStack,
Icon,
Input,
Stat,
StatLabel,
StatNumber,
StatHelpText,
StatArrow,
StatGroup,
Tooltip,
} from "@chakra-ui/react";
import prettifyStatisticName from "./PrettifyStatisticName"; import prettifyStatisticName from "./PrettifyStatisticName";
const randomColor = () => const randomColor = () =>
@ -33,15 +18,15 @@ const StackedBarSegment = ({ aggregate, total }) => {
hasArrow hasArrow
label={`${aggregate.value label={`${aggregate.value
.toString() .toString()
.replace( .replace(/\B(?=(\d{3})+(?!\d))/g, ",")} ${prettifyStatisticName(
/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, aggregate.type,
"," aggregate.name
)} ${prettifyStatisticName(aggregate.type, aggregate.name)}`} )}`}
> >
<Box <Box
width={aggregate.value / total} width={aggregate.value / total}
height="100%" height="100%"
filter={isHovered ? "clear" : "saturate(0.15)"} filter={isHovered ? "clear" : "saturate(0.5)"}
backgroundColor={color} backgroundColor={color}
ref={hoverRef} ref={hoverRef}
></Box> ></Box>

View File

@ -15,19 +15,13 @@ import { Link } from "react-router-dom";
import prettifyStatisticName from "./PrettifyStatisticName"; import prettifyStatisticName from "./PrettifyStatisticName";
const Statistic = ({ type, name, players }) => { const Statistic = ({ type, name, players }) => {
const ranking = useQuery( const ranking = useQuery(`statistic ${type} ${name}`, async () => {
`statistic ${type} ${name}`, const { data } = await axios.get(`/api/statistics/${type}/${name}`);
async () => { return data.filter((x) => x.value > 0);
const { data } = await axios.get(`/api/statistics/${type}/${name}`); });
return data;
},
{
placeholderData: [],
}
);
const playerDict = useMemo(() => { const playerDict = useMemo(() => {
return Object.assign({}, ...players.data.map((x) => ({ [x.id]: x.name }))); return Object.assign({}, ...players.map((x) => ({ [x.id]: x.name })));
}, [players]); }, [players]);
return ( return (
@ -52,19 +46,23 @@ const Statistic = ({ type, name, players }) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ranking.data.map((x, i) => { {ranking.isSuccess ? (
return ( ranking.data.map((x, i) => {
<Tr key={i}> return (
<Td> <Tr key={i}>
<Link to={`/player/${x.playerId}`}> <Td>
{playerDict[x.playerId]} <Link to={`/player/${x.playerId}`}>
</Link> {playerDict[x.playerId]}
</Td> </Link>
<Td isNumeric>{x.rank}</Td> </Td>
<Td isNumeric>{x.value}</Td> <Td isNumeric>{x.rank}</Td>
</Tr> <Td isNumeric>{x.value}</Td>
); </Tr>
})} );
})
) : (
<></>
)}
</Tbody> </Tbody>
</Table> </Table>
</> </>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
function useHover() { function useHover() {
const [value, setValue] = useState(false); const [value, setValue] = useState(false);
@ -16,8 +16,8 @@ function useHover() {
node.removeEventListener("mouseout", handleMouseOut); node.removeEventListener("mouseout", handleMouseOut);
}; };
} }
}, } //,
[ref.current] // Recall only if ref changes //[ref.current] // Recall only if ref changes
); );
return [ref, value]; return [ref, value];
} }