Compare commits
9 Commits
release-0.
...
main
Author | SHA1 | Date |
---|---|---|
Kevin Belisle | 5b7885368f | |
Kevin Belisle | d224ac81d8 | |
Kevin Belisle | c3286bfa7f | |
Kevin Belisle | 7c637f181e | |
Kevin Belisle | 34e5e207d9 | |
Kevin Belisle | 8e27e95fd9 | |
Kevin Belisle | 6b3f87c401 | |
Kevin Belisle | 24d33afd59 | |
Kevin Belisle | a86453f497 |
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
ON Live."Type" = Agg."Type"
|
|
||||||
WHERE array_contains(array['minecraft:mined'], Live."Type")
|
|
||||||
GROUP BY Live."Type"
|
|
||||||
HAVING sum(Live."Value") <> max(Agg."Value") OR max(Agg."Value") IS NULL;
|
|
||||||
|
|
||||||
INSERT INTO AGGREGATESTATISTICS ("Type", "Name", "Timestamp", "Value")
|
|
||||||
SELECT Live."Type",
|
SELECT Live."Type",
|
||||||
Live."Name",
|
Live."Name",
|
||||||
@Timestamp AS "Timestamp",
|
MAX(Live."Value") AS "Value"
|
||||||
Sum(Live."Value")
|
FROM livestatistics as Live
|
||||||
|
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")
|
||||||
|
SELECT LiveMax."Type",
|
||||||
|
LiveMax."Name",
|
||||||
|
@Timestamp AS "Timestamp",
|
||||||
|
SUM(LiveMax."Value")
|
||||||
|
FROM (
|
||||||
|
SELECT Live."Type",
|
||||||
|
Live."Name",
|
||||||
|
MAX(Live."Value") AS "Value"
|
||||||
FROM livestatistics as Live
|
FROM livestatistics as Live
|
||||||
LEFT JOIN AGGREGATESTATISTICS as Agg
|
|
||||||
ON Live."Type" = Agg."Type" AND Live."Name" = Agg."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")
|
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")
|
||||||
OR array_contains(array['minecraft:killed', 'minecraft:killed_by'], Live."Type")
|
OR array_contains(array['minecraft:killed', 'minecraft:killed_by'], Live."Type")
|
||||||
GROUP BY Live."Type", Live."Name"
|
GROUP BY Live."Type", Live."Name", Live."PlayerId"
|
||||||
HAVING sum(Live."Value") <> max(Agg."Value") OR max(Agg."Value") IS NULL;
|
) 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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`,
|
|
||||||
async () => {
|
|
||||||
const { data } = await axios.get(`/api/aggregates`);
|
const { data } = await axios.get(`/api/aggregates`);
|
||||||
return data;
|
return data;
|
||||||
},
|
});
|
||||||
{
|
|
||||||
placeholderData: [],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const killedAggregates = aggregates.data
|
const killedAggregates = aggregates.isSuccess
|
||||||
|
? aggregates.data
|
||||||
.filter((x) => x.type === "minecraft:killed")
|
.filter((x) => x.type === "minecraft:killed")
|
||||||
.sort((a, b) => b.value - a.value);
|
.sort((a, b) => b.value - a.value)
|
||||||
const killedTotal = killedAggregates.reduce((acc, val) => acc + val.value, 0);
|
: [];
|
||||||
|
// const killedTotal = killedAggregates.reduce((acc, val) => acc + val.value, 0);
|
||||||
|
|
||||||
const killedByAggregates = aggregates.data
|
const killedByAggregates = aggregates.isSuccess
|
||||||
|
? aggregates.data
|
||||||
.filter((x) => x.type === "minecraft:killed_by")
|
.filter((x) => x.type === "minecraft:killed_by")
|
||||||
.sort((a, b) => b.value - a.value);
|
.sort((a, b) => b.value - a.value)
|
||||||
const killedByTotal = killedByAggregates.reduce(
|
: [];
|
||||||
(acc, val) => acc + val.value,
|
// const killedByTotal = killedByAggregates.reduce(
|
||||||
0
|
// (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,13 +57,13 @@ 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(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</StatNumber>
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</StatNumber>
|
||||||
</Stat>
|
</Stat>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}`,
|
|
||||||
async () => {
|
|
||||||
const { data } = await axios.get(`/api/players/${playerId}`);
|
const { data } = await axios.get(`/api/players/${playerId}`);
|
||||||
data.sort((a, b) => a.rank - b.rank);
|
data.sort((a, b) => a.rank - b.rank || b.value - a.value);
|
||||||
return data.filter((x) => x.value > 0);
|
return data.filter((x) => x.value > 0);
|
||||||
},
|
});
|
||||||
{
|
|
||||||
placeholderData: [],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const playerName = players.data.find((x) => x.id === playerId)?.name;
|
const playerName = players?.find((x) => x.id === playerId)?.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -60,7 +54,8 @@ const Player = ({ playerId, players }) => {
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{playerStats.data.map((x, i) => {
|
{playerStats.isSuccess ? (
|
||||||
|
playerStats.data.map((x, i) => {
|
||||||
return (
|
return (
|
||||||
<Tr key={i}>
|
<Tr key={i}>
|
||||||
<Td>
|
<Td>
|
||||||
|
@ -72,7 +67,10 @@ const Player = ({ playerId, players }) => {
|
||||||
<Td isNumeric>{x.value}</Td>
|
<Td isNumeric>{x.value}</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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 = () =>
|
||||||
|
@ -41,7 +26,7 @@ const StackedBarSegment = ({ aggregate, total }) => {
|
||||||
<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>
|
||||||
|
|
|
@ -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}`,
|
|
||||||
async () => {
|
|
||||||
const { data } = await axios.get(`/api/statistics/${type}/${name}`);
|
const { data } = await axios.get(`/api/statistics/${type}/${name}`);
|
||||||
return data.filter((x) => x.value > 0);
|
return data.filter((x) => x.value > 0);
|
||||||
},
|
});
|
||||||
{
|
|
||||||
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,7 +46,8 @@ const Statistic = ({ type, name, players }) => {
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{ranking.data.map((x, i) => {
|
{ranking.isSuccess ? (
|
||||||
|
ranking.data.map((x, i) => {
|
||||||
return (
|
return (
|
||||||
<Tr key={i}>
|
<Tr key={i}>
|
||||||
<Td>
|
<Td>
|
||||||
|
@ -64,7 +59,10 @@ const Statistic = ({ type, name, players }) => {
|
||||||
<Td isNumeric>{x.value}</Td>
|
<Td isNumeric>{x.value}</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue