Wallet - bonding, new node stats component (#1535)

* feature(wallet-bonding): add bond more skeleton

* fix(wallet-bonding): post godzilla merge

fix: post rebase

feature(wallet-bonding): wip

* feat(wallet-bonding): add node stats component

feat(wallet-bonding): add node stats component

* feat(wallet-bonding): fix tooltip icon size

* fix(wallet-bonding): node stats fix responsiveness

fix(wallet-bonding): post godzilla merge

* feat(wallet): fetch active set probabilities

* feat(wallet): operation mixnode avg uptime

* fix: typo

* fix(wallet): theme colors

fix(wallet): theme colors

* fix(wallet): destructuring

* feat(wallet): request total reward data

* refactor(wallet): clean code

* fix(wallet): typo, better error logging

* fix(wallet): some nym decimal values

* fix(wallet): deal with unym nym values

* fix(wallet): routing score chart

* feat(wallet): new node stats design

* feat(wallet): new node stats design

* fix(wallet): increase component width

* fix(wallet): some vertical alignements
This commit is contained in:
Pierre Dommerc
2022-10-14 10:40:53 +02:00
committed by GitHub
parent 732235afc0
commit da95e4e903
15 changed files with 495 additions and 83 deletions
+3
View File
@@ -34,6 +34,7 @@
"@storybook/react": "^6.5.8",
"@tauri-apps/api": "^1.0.2",
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
"big.js": "^6.2.1",
"bs58": "^4.0.1",
"clsx": "^1.1.1",
"date-fns": "^2.28.0",
@@ -46,6 +47,7 @@
"react-error-boundary": "^3.1.3",
"react-hook-form": "^7.14.2",
"react-router-dom": "6",
"recharts": "^2.1.13",
"semver": "^6.3.0",
"string-to-color": "^2.2.2",
"use-clipboard-copy": "^0.2.0",
@@ -65,6 +67,7 @@
"@tauri-apps/cli": "^1.0.5",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/big.js": "^6.1.6",
"@types/bs58": "^4.0.1",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
+1
View File
@@ -64,6 +64,7 @@ fn main() {
mixnet::bond::update_mixnode_config,
mixnet::bond::get_number_of_mixnode_delegators,
mixnet::bond::get_mix_node_description,
mixnet::bond::get_mixnode_avg_uptime,
mixnet::delegate::delegate_to_mixnode,
mixnet::delegate::get_pending_delegator_rewards,
mixnet::delegate::get_pending_delegation_events,
@@ -170,6 +170,32 @@ pub async fn update_mixnode_config(
)?)
}
#[tauri::command]
pub async fn get_mixnode_avg_uptime(
state: tauri::State<'_, WalletState>,
) -> Result<Option<u8>, BackendError> {
log::info!(">>> Get mixnode bond details");
let guard = state.read().await;
let client = guard.current_client()?;
let res = client.nymd.get_owned_mixnode(client.nymd.address()).await?;
match res.mixnode_details {
Some(details) => {
let id = details.mix_id();
log::trace!(" >>> Get average uptime percentage: mix_id = {}", id);
let avg_uptime_percent = client
.validator_api
.get_mixnode_avg_uptime(id)
.await
.ok()
.map(|r| r.avg_uptime);
log::trace!(" <<< {:?}", avg_uptime_percent);
Ok(avg_uptime_percent)
}
None => Ok(None),
}
}
#[tauri::command]
pub async fn mixnode_bond_details(
state: tauri::State<'_, WalletState>,
@@ -1,16 +1,16 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Button, Stack, Typography } from '@mui/material';
import { Box, Button, Stack, Tooltip, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { isMixnode } from 'src/types';
import { isMixnode, Network } from 'src/types';
import { TBondedMixnode, urls } from 'src/context';
import { NymCard } from 'src/components';
import { Network } from 'src/types';
import { IdentityKey } from 'src/components/IdentityKey';
import { NodeStatus } from 'src/components/NodeStatus';
import { Node as NodeIcon } from '../../svg-icons/node';
import { Cell, Header, NodeTable } from './NodeTable';
import { BondedMixnodeActions, TBondedMixnodeActions } from './BondedMixnodeActions';
import { NodeStats } from './NodeStats';
const headers: Header[] = [
{
@@ -74,6 +74,7 @@ export const BondedMixnode = ({
delegators,
status,
identityKey,
host,
} = mixnode;
const cells: Cell[] = [
{
@@ -93,7 +94,7 @@ export const BondedMixnode = ({
id: 'pm-cell',
},
{
cell: operatorCost ? `${operatorCost} USD` : '-',
cell: operatorCost ? `${operatorCost} NYM` : '-',
id: 'operator-cost-cell',
},
{
@@ -109,6 +110,7 @@ export const BondedMixnode = ({
<BondedMixnodeActions
onActionSelect={onActionSelect}
disabledRedeemAndCompound={(operatorRewards && Number(operatorRewards.amount) === 0) || false}
disabledBondMore // TODO for now disable bond more feature until backend is ready
/>
),
id: 'actions-cell',
@@ -117,46 +119,51 @@ export const BondedMixnode = ({
];
return (
<NymCard
borderless
title={
<Stack gap={3}>
<Box display="flex" alignItems="center" gap={3}>
<Typography variant="h5" fontWeight={600}>
Mix node
</Typography>
<NodeStatus status={status} />
</Box>
{name && (
<Typography fontWeight="regular" variant="h6">
{name}
</Typography>
)}
<IdentityKey identityKey={identityKey} />
</Stack>
}
Action={
isMixnode(mixnode) && (
<Button
variant="text"
color="secondary"
onClick={() => navigate('/bonding/node-settings')}
startIcon={<NodeIcon />}
>
Settings
</Button>
)
}
>
<NodeTable headers={headers} cells={cells} />
{network && (
<Typography sx={{ mt: 2, fontSize: 'small' }}>
Check more stats of your node on the{' '}
<Link href={`${urls(network).networkExplorer}/network-components/mixnodes`} target="_blank">
explorer
</Link>
</Typography>
)}
</NymCard>
<Stack gap={2}>
<NymCard
borderless
title={
<Stack gap={3}>
<Box display="flex" alignItems="center" gap={2}>
<Typography variant="h5" fontWeight={600}>
Mix node
</Typography>
<NodeStatus status={status} />
</Box>
{name && (
<Tooltip title={host} arrow>
<Typography fontWeight="regular" variant="h6">
{name}
</Typography>
</Tooltip>
)}
<IdentityKey identityKey={identityKey} />
</Stack>
}
Action={
isMixnode(mixnode) && (
<Button
variant="text"
color="secondary"
onClick={() => navigate('/bonding/node-settings')}
startIcon={<NodeIcon />}
>
Node Settings
</Button>
)
}
>
<NodeTable headers={headers} cells={cells} />
{network && (
<Typography sx={{ mt: 2, fontSize: 'small' }}>
Check more stats of your node on the{' '}
<Link href={`${urls(network).networkExplorer}/network-components/mixnodes`} target="_blank">
explorer
</Link>
</Typography>
)}
</NymCard>
<NodeStats mixnode={mixnode} />
</Stack>
);
};
@@ -8,9 +8,11 @@ export type TBondedMixnodeActions = 'nodeSettings' | 'bondMore' | 'unbond' | 're
export const BondedMixnodeActions = ({
onActionSelect,
disabledRedeemAndCompound,
disabledBondMore,
}: {
onActionSelect: (action: TBondedMixnodeActions) => void;
disabledRedeemAndCompound: boolean;
disabledBondMore?: boolean;
}) => {
const [isOpen, setIsOpen] = useState(false);
@@ -24,11 +26,13 @@ export const BondedMixnodeActions = ({
return (
<ActionsMenu open={isOpen} onOpen={handleOpen} onClose={handleClose}>
<ActionsMenuItem
title="Bond More"
Icon={<BondIcon fontSize="inherit" />}
onClick={() => handleActionClick('bondMore')}
/>
{!disabledBondMore && (
<ActionsMenuItem
title="Bond More"
Icon={<BondIcon fontSize="inherit" />}
onClick={() => handleActionClick('bondMore')}
/>
)}
<ActionsMenuItem
title="Redeem rewards"
Icon={<Typography sx={{ pl: 1, fontWeight: 700 }}>R</Typography>}
@@ -0,0 +1,142 @@
import React from 'react';
import { Stack, Typography, Box, useTheme, Grid, LinearProgress, LinearProgressProps } from '@mui/material';
import { TBondedMixnode } from 'src/context';
import { Cell, Pie, PieChart, Legend, ResponsiveContainer } from 'recharts';
import { SelectionChance } from '@nymproject/types';
import { NymCard } from '../NymCard';
import { InfoTooltip } from '../InfoToolTip';
const LinearProgressWithLabel = (props: LinearProgressProps & { value: number }) => {
const { value } = props;
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ minWidth: 30 }}>
<Typography>{`${Math.round(value)}%`}</Typography>
</Box>
<Box sx={{ width: 40 }}>
<LinearProgress sx={{ borderRadius: 4 }} color="success" variant="determinate" {...props} />
</Box>
</Box>
);
};
const StatRow = ({
label,
tooltipText,
value,
textColor,
progressValue,
}: {
label: string;
tooltipText?: string;
value: string | number;
textColor?: string;
progressValue?: number;
}) => (
<Stack direction="row" gap={1} justifyContent="space-between" alignItems="center" width="100%">
<Stack direction="row" alignItems="center" gap={1} sx={{ color: (t) => t.palette.nym.text.muted }}>
{tooltipText && <InfoTooltip title={tooltipText} />}
<Typography>{label}</Typography>
</Stack>
{typeof progressValue === 'number' ? (
<LinearProgressWithLabel value={progressValue} />
) : (
<Typography color={textColor}>{value}</Typography>
)}
</Stack>
);
export const NodeStats = ({ mixnode }: { mixnode: TBondedMixnode }) => {
const { activeSetProbability, routingScore } = mixnode;
const theme = useTheme();
// clamp routing score to [0-100]
const score = Math.min(Math.max(routingScore, 0), 100);
const data = [
{ key: 'routingScore', value: score },
{ key: 'rest', value: 100 - score },
];
const colors = [theme.palette.success.main, theme.palette.nym.nymWallet.chart.grey];
const getSetProbabilityLabel = (chance?: SelectionChance): { value: string; color?: string } => {
switch (chance) {
case 'High':
return { value: 'High', color: theme.palette.nym.success };
case 'Good':
return { value: 'Good' };
case 'Low':
return { value: 'Low', color: theme.palette.nym.red };
default:
return { value: 'Unknown' };
}
};
const renderLegend = () => (
<Stack
alignItems="center"
maxWidth={200}
width={200}
sx={{
borderBottom: `1px solid ${theme.palette.nym.nymWallet.chart.grey}`,
}}
>
<Typography fontWeight={600} fontSize={24} mb={1}>
{routingScore}%
</Typography>
</Stack>
);
const activeSetProb = getSetProbabilityLabel(activeSetProbability);
return (
<Grid container spacing={4} direction="row" justifyContent="space-between" alignItems="flex-end">
<Grid item xs={12} sm={8} md={7} lg={5}>
<NymCard
borderless
title={
<Typography variant="h5" fontWeight={600}>
Node stats
</Typography>
}
>
<Stack justifyContent="center" alignItems="center" mb={2}>
<ResponsiveContainer width="100%" height={100}>
<PieChart width={200} height={100}>
<Pie
cy={90}
data={data}
startAngle={180}
endAngle={0}
innerRadius={58}
outerRadius={78}
dataKey="value"
stroke="none"
>
{data.map(({ key }, index) => (
<Cell key={`cell-${key}`} fill={colors[index]} />
))}
</Pie>
<Legend
verticalAlign="bottom"
content={renderLegend}
wrapperStyle={{
display: 'flex',
justifyContent: 'center',
}}
/>
</PieChart>
</ResponsiveContainer>
<Typography color="nym.text.muted">Routing score</Typography>
</Stack>
<StatRow
label="Chance of being in the active set"
value={activeSetProb.value}
textColor={activeSetProb.color}
/>
</NymCard>
</Grid>
<Grid item xs={12} sm={4} md={5} lg={7} />
</Grid>
);
};
@@ -28,7 +28,7 @@ export const NodeTable = ({ headers, cells }: TableProps) => (
<TableRow>
{headers.map(({ header, id, tooltipText }) => (
<TableCell key={id}>
<Stack direction="row" gap={1}>
<Stack direction="row" alignItems="center" gap={1}>
{tooltipText && <InfoTooltip title={tooltipText} />}
<Typography>{header}</Typography>
</Stack>
+2 -2
View File
@@ -6,12 +6,12 @@ export const InfoTooltip = ({
title,
tooltipPlacement = 'bottom',
light,
size = 'small',
size = 'inherit',
}: {
title: string;
tooltipPlacement?: TooltipProps['placement'];
light?: boolean;
size?: 'small' | 'medium' | 'large';
size?: 'small' | 'medium' | 'large' | 'inherit';
}) => (
<Tooltip title={title} arrow placement={tooltipPlacement}>
<InfoOutlined fontSize={size} sx={{ color: light ? 'grey.500' : undefined }} />
+87 -20
View File
@@ -3,10 +3,11 @@ import {
DecCoin,
MixnodeStatus,
TransactionExecuteResult,
decimalToFloatApproximation,
decimalToPercentage,
SelectionChance,
} from '@nymproject/types';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import Big from 'big.js';
import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs } from 'src/types';
import { Console } from 'src/utils/console';
import {
@@ -24,17 +25,24 @@ import {
vestingUnbondMixnode,
updateMixnodeCostParams as updateMixnodeCostParamsRequest,
vestingUpdateMixnodeCostParams as updateMixnodeVestingCostParamsRequest,
getNodeDescription as getNodeDescriptioRequest,
getNodeDescription as getNodeDescriptionRequest,
getMixnodeStatus,
getPendingOperatorRewards,
getMixnodeStakeSaturation,
vestingClaimOperatorReward,
getInclusionProbability,
getMixnodeAvgUptime,
} from '../requests';
import { useCheckOwnership } from '../hooks/useCheckOwnership';
import { AppContext } from './main';
import { attachDefaultOperatingCost, toPercentFloatString, toPercentIntegerString } from '../utils';
import {
attachDefaultOperatingCost,
toDisplay,
toPercentFloatString,
toPercentIntegerString,
unymToNym,
} from '../utils';
// TODO add relevant data
export type TBondedMixnode = {
name?: string;
identityKey: string;
@@ -46,12 +54,16 @@ export type TBondedMixnode = {
delegators: number;
status: MixnodeStatus;
proxy?: string;
operatorCost?: string;
host: string;
estimatedRewards?: DecCoin;
activeSetProbability?: SelectionChance;
standbySetProbability?: SelectionChance;
routingScore: number;
httpApiPort: number;
mixPort: number;
verlocPort: number;
version: string;
operatorCost?: string;
};
export interface TBondedGateway {
@@ -84,9 +96,6 @@ export type TBondingContext = {
checkOwnership: () => Promise<void>;
};
const calculateStake = (pledge: string, delegations: string): number =>
decimalToFloatApproximation(pledge) + decimalToFloatApproximation(delegations);
export const BondingContext = createContext<TBondingContext>({
isLoading: true,
refresh: async () => undefined,
@@ -129,7 +138,12 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
};
const getAdditionalMixnodeDetails = async (mixId: number) => {
const additionalDetails: { status: MixnodeStatus; stakeSaturation: string; operatorCost?: string } = {
const additionalDetails: {
status: MixnodeStatus;
stakeSaturation: string;
operatorCost?: string;
estimatedRewards?: DecCoin;
} = {
status: 'not_found',
stakeSaturation: '0',
};
@@ -138,31 +152,69 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
const statusResponse = await getMixnodeStatus(mixId);
additionalDetails.status = statusResponse.status;
} catch (e) {
Console.log(e);
Console.log('getMixnodeStatus fails', e);
}
try {
const stakeSaturationResponse = await getMixnodeStakeSaturation(mixId);
additionalDetails.stakeSaturation = decimalToPercentage(stakeSaturationResponse.saturation);
} catch (e) {
Console.log(e);
Console.log('getMixnodeStakeSaturation fails', e);
}
try {
const rewardEstimation = await getMixnodeRewardEstimation(mixId);
additionalDetails.operatorCost = rewardEstimation.estimation.operating_cost;
additionalDetails.operatorCost = unymToNym(rewardEstimation.estimation.operating_cost);
const estimatedRewards = unymToNym(rewardEstimation.estimation.total_node_reward);
if (estimatedRewards) {
additionalDetails.estimatedRewards = {
amount: estimatedRewards,
denom: 'nym',
};
}
} catch (e) {
Console.log(e);
Console.log('getMixnodeRewardEstimation fails', e);
}
return additionalDetails;
};
const getNodeDescription = async (host: string, port: number) => {
let result;
try {
return await getNodeDescriptioRequest(host, port);
result = await getNodeDescriptionRequest(host, port);
} catch (e) {
Console.log(e);
Console.log('getNodeDescriptionRequest fails', e);
}
return undefined;
return result;
};
const getSetProbabilities = async (mixId: number) => {
let result;
try {
result = await getInclusionProbability(mixId);
} catch (e: any) {
Console.log('getInclusionProbability fails', e);
}
return result;
};
const getAvgUptime = async () => {
let result;
try {
result = await getMixnodeAvgUptime();
} catch (e: any) {
Console.log('getMixnodeAvgUptime fails', e);
}
return result;
};
const calculateStake = (pledge: string, delegations: string) => {
let stake;
try {
stake = unymToNym(Big(pledge).plus(delegations));
} catch (e: any) {
Console.warn(`not a valid decimal number: ${e}`);
}
return stake;
};
const refresh = useCallback(async () => {
@@ -174,22 +226,33 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
let operatorRewards;
try {
operatorRewards = await getPendingOperatorRewards(clientDetails?.client_address);
const opRewards = toDisplay(operatorRewards.amount);
if (opRewards) {
operatorRewards.amount = opRewards;
}
} catch (e) {
Console.warn(`get_operator_rewards request failed: ${e}`);
}
if (data) {
const { bond_information, rewarding_details } = data;
const { status, stakeSaturation, operatorCost } = await getAdditionalMixnodeDetails(data.bond_information.id);
const {
bond_information,
rewarding_details,
bond_information: { id },
} = data;
const { status, stakeSaturation, operatorCost, estimatedRewards } = await getAdditionalMixnodeDetails(id);
const setProbabilities = await getSetProbabilities(id);
const nodeDescription = await getNodeDescription(
bond_information.mix_node.host,
bond_information.mix_node.http_api_port,
);
const routingScore = await getAvgUptime();
setBondedNode({
name: nodeDescription?.name,
identityKey: bond_information.mix_node.identity_key,
ip: bond_information.id,
stake: {
amount: calculateStake(rewarding_details.operator, data.rewarding_details.delegates).toString(),
amount: calculateStake(rewarding_details.operator, rewarding_details.delegates),
denom: bond_information.original_pledge.denom,
},
bond: bond_information.original_pledge,
@@ -199,12 +262,16 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
operatorRewards,
status,
stakeSaturation,
operatorCost,
host: bond_information.mix_node.host.replace(/\s/g, ''),
routingScore,
activeSetProbability: setProbabilities?.in_active,
standbySetProbability: setProbabilities?.in_reserve,
estimatedRewards,
httpApiPort: bond_information.mix_node.http_api_port,
mixPort: bond_information.mix_node.mix_port,
verlocPort: bond_information.mix_node.verloc_port,
version: bond_information.mix_node.version,
operatorCost,
} as TBondedMixnode);
}
} catch (e: any) {
+5 -1
View File
@@ -16,8 +16,12 @@ const bondedMixnodeMock: TBondedMixnode = {
operatorRewards: { denom: 'nym', amount: '1234' },
delegators: 5423,
status: 'active',
operatorCost: '2',
operatorCost: '0.22',
host: '1.2.3.4',
routingScore: 75,
activeSetProbability: 'High',
standbySetProbability: 'Low',
estimatedRewards: { denom: 'nym', amount: '2' },
httpApiPort: 8000,
mixPort: 1789,
verlocPort: 1790,
+1
View File
@@ -16,6 +16,7 @@ export const getAllPendingDelegations = async () =>
export const getMixnodeBondDetails = async () => invokeWrapper<MixNodeDetails | null>('mixnode_bond_details');
export const getGatewayBondDetails = async () => invokeWrapper<GatewayBond | null>('gateway_bond_details');
export const getMixnodeAvgUptime = async () => invokeWrapper<number | null>('get_mixnode_avg_uptime');
export const getPendingOperatorRewards = async (address: string) =>
invokeWrapper<DecCoin>('get_pending_operator_rewards', { address });
+8 -5
View File
@@ -73,6 +73,9 @@ declare module '@mui/material/styles' {
modal: {
border: string;
};
chart: {
grey: string;
};
}
/**
@@ -91,7 +94,7 @@ declare module '@mui/material/styles' {
/**
* Add anything not palette related to the theme here
*/
interface NymTheme { }
interface NymTheme {}
/**
* This augments the definitions of the MUI Theme with the Nym theme, as well as
@@ -99,8 +102,8 @@ declare module '@mui/material/styles' {
*
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
*/
interface Theme extends NymTheme { }
interface ThemeOptions extends Partial<NymTheme> { }
interface Palette extends NymPaletteAndNymWalletPalette { }
interface PaletteOptions extends NymPaletteAndNymWalletPaletteOptions { }
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteAndNymWalletPalette {}
interface PaletteOptions extends NymPaletteAndNymWalletPaletteOptions {}
}
+2
View File
@@ -67,6 +67,7 @@ const darkMode: NymPaletteVariant = {
modal: {
border: '#484d53',
},
chart: { grey: '#3D4249' },
};
const lightMode: NymPaletteVariant = {
@@ -98,6 +99,7 @@ const lightMode: NymPaletteVariant = {
modal: {
border: 'transparent',
},
chart: { grey: '#E6E6E6' },
};
/**
+36
View File
@@ -1,5 +1,6 @@
import { appWindow } from '@tauri-apps/api/window';
import bs58 from 'bs58';
import Big from 'big.js';
import { valid } from 'semver';
import { isValidRawCoin, DecCoin, MixNodeCostParams } from '@nymproject/types';
import { TPoolOption } from 'src/components';
@@ -142,3 +143,38 @@ export const toPercentFloatString = (value: string) => (Number(value) / 100).toS
* @returns A stringified integer
*/
export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString();
/**
* Converts a decimal number of NYM to a pretty representation
* with fixed decimal places.
*
* @param nym - a decimal number of NYM
* @param dp - number of decimal places (4 by default ie. 0.0000)
* @returns A prettyfied decimal number
*/
export const toDisplay = (nym: string | number | Big, dp = 4) => {
let displayValue;
try {
displayValue = Big(nym).toFixed(dp);
} catch (e: any) {
Console.warn(`${displayValue} not a valid decimal number: ${e}`);
}
return displayValue;
};
/**
* Converts a decimal number of μNYM (micro NYM) to NYM.
*
* @param unym - string representation of a decimal number of μNYM
* @param dp - number of decimal places (4 by default ie. 0.0000)
* @returns The corresponding decimal number in NYM
*/
export const unymToNym = (unym: string | Big, dp = 4) => {
let nym;
try {
nym = Big(unym).div(1_000_000).toFixed(dp);
} catch (e: any) {
Console.warn(`${unym} not a valid decimal number: ${e}`);
}
return nym;
};
+120 -4
View File
@@ -5054,6 +5054,11 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/big.js@^6.1.6":
version "6.1.6"
resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.1.6.tgz#3d417e758483d55345a03a087f7e0c87137ca444"
integrity sha512-0r9J+Zz9rYm2hOTwiMAVkm3XFQ4u5uTK37xrQMhc9bysn/sf/okzovWMYYIBMFTn/yrEZ11pusgLEaoarTlQbA==
"@types/body-parser@*":
version "1.19.2"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
@@ -6975,6 +6980,11 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
big.js@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.1.tgz#7205ce763efb17c2e41f26f121c420c6a7c2744f"
integrity sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ==
bignumber.js@^2.1.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-2.4.0.tgz#838a992da9f9d737e0f4b2db0be62bb09dd0c5e8"
@@ -7667,6 +7677,11 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.5:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
clean-css@^4.2.3:
version "4.2.4"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
@@ -8406,6 +8421,11 @@ css-tree@^1.1.2, css-tree@^1.1.3:
mdn-data "2.0.14"
source-map "^0.6.1"
css-unit-converter@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21"
integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==
css-vendor@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d"
@@ -8599,14 +8619,19 @@ d3-geo@^2.0.1:
dependencies:
d3-color "1 - 2"
"d3-interpolate@1.2.0 - 3":
"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
dependencies:
d3-color "1 - 3"
d3-scale@^4.0.0:
"d3-path@1 - 3":
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e"
integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==
d3-scale@^4.0.0, d3-scale@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
@@ -8622,6 +8647,13 @@ d3-selection@2, d3-selection@^2.0.0:
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-2.0.0.tgz#94a11638ea2141b7565f883780dabc7ef6a61066"
integrity sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==
d3-shape@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556"
integrity sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==
dependencies:
d3-path "1 - 3"
"d3-time-format@2 - 4":
version "4.1.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
@@ -8750,6 +8782,11 @@ decamelize@^4.0.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
decimal.js-light@^2.4.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
decimal.js@^10.2.1:
version "10.3.1"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
@@ -9082,6 +9119,13 @@ dom-converter@^0.2.0:
dependencies:
utila "~0.4"
dom-helpers@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
dependencies:
"@babel/runtime" "^7.1.2"
dom-helpers@^5.0.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
@@ -9834,7 +9878,7 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
eventemitter3@^4.0.0, eventemitter3@^4.0.4:
eventemitter3@^4.0.0, eventemitter3@^4.0.1, eventemitter3@^4.0.4:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
@@ -10033,6 +10077,11 @@ fast-diff@^1.1.2:
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-equals@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.4.tgz#3add9410585e2d7364c2deeb6a707beadb24b927"
integrity sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w==
fast-glob@^2.2.6:
version "2.2.7"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
@@ -15521,6 +15570,11 @@ postcss-unique-selectors@^5.1.0:
dependencies:
postcss-selector-parser "^6.0.5"
postcss-value-parser@^3.3.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
@@ -16129,7 +16183,7 @@ react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
react-is@^16.10.2, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -16139,6 +16193,11 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-load-script@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/react-load-script/-/react-load-script-0.0.6.tgz#db6851236aaa25bb622677a2eb51dad4f8d2c258"
@@ -16164,6 +16223,13 @@ react-refresh@^0.11.0:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
react-resize-detector@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-7.1.2.tgz#8ef975dd8c3d56f9a5160ac382ef7136dcd2d86c"
integrity sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==
dependencies:
lodash "^4.17.21"
react-router-dom@6:
version "6.3.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d"
@@ -16233,6 +16299,14 @@ react-simple-maps@^2.3.0:
d3-zoom "^2.0.0"
topojson-client "^3.1.0"
react-smooth@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-2.0.1.tgz#74c7309916d6ccca182c4b30c8992f179e6c5a05"
integrity sha512-Own9TA0GPPf3as4vSwFhDouVfXP15ie/wIHklhyKBH5AN6NFtdk0UpHBnonV11BtqDkAWlt40MOUc+5srmW7NA==
dependencies:
fast-equals "^2.0.0"
react-transition-group "2.9.0"
react-syntax-highlighter@^15.4.5:
version "15.5.0"
resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
@@ -16252,6 +16326,16 @@ react-tooltip@*, react-tooltip@^4.2.21:
prop-types "^15.7.2"
uuid "^7.0.3"
react-transition-group@2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
dependencies:
dom-helpers "^3.4.0"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react-transition-group@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
@@ -16440,6 +16524,30 @@ readonly-date@^1.0.0:
resolved "https://registry.yarnpkg.com/readonly-date/-/readonly-date-1.0.0.tgz#5af785464d8c7d7c40b9d738cbde8c646f97dcd9"
integrity sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==
recharts-scale@^0.4.4:
version "0.4.5"
resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9"
integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==
dependencies:
decimal.js-light "^2.4.1"
recharts@^2.1.13:
version "2.1.13"
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.1.13.tgz#61801acf3e13896b07dc6a8b38cbdd648480d0b7"
integrity sha512-9VWu2nzExmfiMFDHKqRFhYlJVmjzQGVKH5rBetXR4EuyEXuu3Y6cVxQuNEdusHhbm4SoPPrVDCwlBdREL3sQPA==
dependencies:
classnames "^2.2.5"
d3-interpolate "^3.0.1"
d3-scale "^4.0.2"
d3-shape "^3.1.0"
eventemitter3 "^4.0.1"
lodash "^4.17.19"
react-is "^16.10.2"
react-resize-detector "^7.1.2"
react-smooth "^2.0.1"
recharts-scale "^0.4.4"
reduce-css-calc "^2.1.8"
rechoir@^0.7.0:
version "0.7.1"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686"
@@ -16463,6 +16571,14 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
reduce-css-calc@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03"
integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==
dependencies:
css-unit-converter "^1.1.1"
postcss-value-parser "^3.3.0"
refractor@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a"