Network Explorer: add economic dynamics (#1223)

* adding delegators number info on mixnode details

* add PM, Delegators and Avg. Uptime fields to the node list hardcoded

* make delegations number dynamic

* fixing bg color bug

* wip node info statistics

* adding basic tooltip new section and some ui

* tooltip customisation

* progress bar styles

* remove not used import

* fix info icons color

* remove discord icon

* Economic dynamics stats endpoint on the explorer API with dummy fixture data

* fetching economic-dynamics-stats

* Populating the endpoint with real data aggregated from validator api

* Introduced new cache functionalities

* using explorer-api data

* adding marging profit

* adding average update

* Update network-explorer.yml

* adding more info on mix nodes page

* display only part of wallet and node id

* typo

* remove log

* adding new values on node response and fix a typo

* remove delegators number column

* Endpoints for average mixnode uptime

* remove TODO

* Clippy

* some ui fixes for percentage linear progress

* GitHub Actions: build storybook for the Network Explorer and add to notification

* Fix file extension to `.ts`

* Fix up formatting and types

* Add storybook

* Add story for mix node details economics

* Fix unused warning

* adding percentage symbol on uptime in mix nodes

* Change eslint config

* some refactor

* progress bar story

* wip refactoring

* more refactor

* adding empty state to the story

* change default values for empty state

* refactor naming and progress bar contrast

* adding hardcoded selection chance and update the storybook

* adding selection chance stories

* adding the progress bar back

* tooltip button padding fix

* Endpoints for average mixnode uptime

* Fix unused warning

* Rustfmt

* moking selection chance response and new colors

* remove log

* fix camelCases issue

* remove hardcoded code

* remove avg_uptime at mixnodes table

* Add jsonchema to uptimeresponse struct

- add the route for avg_uptimes

* adding space between words

* update selection chance colours

* adding the 2 missing tooltips

* fix up uptimeresponse

* fix duplicate entry

* fmt

* validator-client: use statement

* explorer: PR requests

Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
Co-authored-by: Fouad <fmtabbara@hotmail.co.uk>
Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
Co-authored-by: Mark Sinclair <mmsinclair@gmail.com>
Co-authored-by: tommy <tommyvez@protonmail.com>
This commit is contained in:
Gala
2022-05-06 21:50:26 +02:00
committed by GitHub
parent f684664472
commit d7e82b075f
29 changed files with 837 additions and 23 deletions
+14
View File
@@ -31,6 +31,8 @@ jobs:
# continue-on-error: true
- run: yarn && yarn build
continue-on-error: true
- run: yarn storybook:build
name: Build storybook
- name: Deploy branch to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
@@ -42,6 +44,17 @@ jobs:
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/network-explorer-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
- name: Deploy storybook to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "explorer/storybook-static/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/ne-sb-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
- name: Keybase - Node Install
run: npm install
working-directory: .github/workflows/support-files
@@ -51,6 +64,7 @@ jobs:
NYM_PROJECT_NAME: "Network Explorer"
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
NYM_CI_WWW_LOCATION: "network-explorer-${{ env.GITHUB_REF_SLUG }}"
NYM_CI_WWW_LOCATION_STORYBOOK: "ne-sb-${{ env.GITHUB_REF_SLUG }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
@@ -1,5 +1,6 @@
🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
> :rocket: {{ env.NYM_PROJECT_NAME }} ➡️➡️➡️➡️➡️ **View output:** https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}/
> `storybook`: https://{{ env.NYM_CI_WWW_LOCATION_STORYBOOK }}.{{ env.NYM_CI_WWW_BASE }}
> ✅ **SUCCESS**
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
extends: [
'@nymproject/eslint-config-react-typescript'
],
overrides: [
{
files: ['*.ts'],
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
}
}
]
}
-5
View File
@@ -1,5 +0,0 @@
{
"extends": [
"@nymproject/eslint-config-react-typescript"
]
}
+63
View File
@@ -0,0 +1,63 @@
/* eslint-disable no-param-reassign */
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: '@storybook/react',
core: {
builder: 'webpack5',
},
// webpackFinal: async (config, { configType }) => {
// // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
// // You can change the configuration based on that.
// // 'PRODUCTION' is used when building the static version of storybook.
webpackFinal: async (config) => {
config.module.rules.forEach((rule) => {
// look for SVG import rule and replace
// NOTE: the rule before modification is /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/
if (rule.test?.toString().includes('svg')) {
rule.test = /\.(ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/;
}
});
// handle asset loading with this
config.module.rules.unshift({
test: /\.svg(\?.*)?$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
});
config.resolve.extensions = ['.tsx', '.ts', '.js'];
config.resolve.plugins = [new TsconfigPathsPlugin()];
config.resolve.fallback = {
fs: false,
tls: false,
path: false,
http: false,
https: false,
stream: false,
crypto: false,
net: false,
zlib: false,
};
config.plugins.push(new ForkTsCheckerWebpackPlugin({
typescript: {
mode: 'write-references',
diagnosticOptions: {
semantic: true,
syntactic: true,
},
},
}));
// Return the altered config
return config;
},
features: {
emotionAlias: false,
},
};
+56
View File
@@ -0,0 +1,56 @@
/* eslint-disable react/react-in-jsx-scope */
import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme';
import { Box } from '@mui/material';
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}
const withThemeProvider = (Story, context) => (
<div style={{ display: 'grid', height: '100%', gridTemplateColumns: '50% 50%' }}>
<div>
<NymNetworkExplorerThemeProvider mode="light">
<Box
p={4}
sx={{
display: 'grid',
gridTemplateRows: '80vh 2rem',
background: (theme) => theme.palette.background.default,
color: (theme) => theme.palette.text.primary,
}}
>
<Box sx={{ overflowY: 'auto' }}>
<Story {...context} />
</Box>
<h4 style={{ textAlign: 'center' }}>Light mode</h4>
</Box>
</NymNetworkExplorerThemeProvider>
</div>
<div>
<NymNetworkExplorerThemeProvider mode="dark">
<Box
p={4}
sx={{
display: 'grid',
gridTemplateRows: '80vh 2rem',
background: (theme) => theme.palette.background.default,
color: (theme) => theme.palette.text.primary,
}}
>
<Box sx={{ overflowY: 'auto' }}>
<Story {...context} />
</Box>
<h4 style={{ textAlign: 'center' }}>Dark mode</h4>
</Box>
</NymNetworkExplorerThemeProvider>
</div>
</div>
);
export const decorators = [withThemeProvider];
+13 -3
View File
@@ -30,8 +30,15 @@
"use-clipboard-copy": "^0.2.0"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@storybook/addon-actions": "^6.4.19",
"@storybook/addon-essentials": "^6.4.19",
"@storybook/addon-interactions": "^6.4.19",
"@storybook/addon-links": "^6.4.19",
"@storybook/react": "^6.4.19",
"@storybook/testing-library": "^0.0.9",
"@svgr/webpack": "^6.1.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
@@ -95,10 +102,13 @@
"build:serve": "npx serve dist",
"test": "jest",
"test:watch": "jest --watch",
"tsc": "tsc",
"tsc:watch": "tsc --watch",
"tsc": "tsc --noEmit true",
"tsc:watch": "tsc --watch --noEmit true",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
"lint:fix": "eslint src --fix",
"prestorybook": "yarn --cwd .. build",
"storybook": "start-storybook -p 6006",
"storybook:build": "build-storybook"
},
"browserslist": {
"production": [
+4
View File
@@ -18,6 +18,7 @@ import {
MixNodeResponse,
MixNodeResponseItem,
MixnodeStatus,
MixNodeEconomicDynamicsStatsResponse,
StatsResponse,
StatusResponse,
SummaryOverviewResponse,
@@ -122,6 +123,9 @@ export class Api {
static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> =>
(await fetch(`${MIXNODE_API}/${id}/description`)).json();
static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> =>
(await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json();
static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json();
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
+3 -2
View File
@@ -12,12 +12,13 @@ export type ColumnsType = {
headerAlign: string;
flex?: number;
width?: number;
tooltipInfo?: string;
};
export interface UniversalTableProps {
export interface UniversalTableProps<T = any> {
tableName: string;
columnsData: ColumnsType[];
rows: any[];
rows: T[];
}
function formatCellValues(val: string | number, field: string) {
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Alert, Box, CircularProgress, useMediaQuery } from '@mui/material';
import { Alert, Box, CircularProgress, useMediaQuery, Typography } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
@@ -127,21 +127,62 @@ export const BondBreakdownTable: React.FC = () => {
sx={{
maxHeight: 400,
overflowY: 'scroll',
p: 2,
background: theme.palette.background.paper,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'baseline',
width: '100%',
p: 2,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
data-testid="delegations-total-amount"
>
<Typography
sx={{
fontSize: 16,
fontWeight: 600,
}}
>
Delegations&nbsp;&nbsp;
</Typography>
<Typography
sx={{
fontSize: 12,
fontWeight: 400,
}}
>
{`(${delegations?.data?.length} delegators)`}
</Typography>
</Box>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 600, background: '#242C3D' }} align="left">
<TableCell
sx={{
fontWeight: 600,
background: theme.palette.background.paper,
}}
align="left"
>
Delegators
</TableCell>
<TableCell sx={{ fontWeight: 600, background: '#242C3D' }} align="left">
<TableCell
sx={{
fontWeight: 600,
background: theme.palette.background.paper,
}}
align="left"
>
Stake
</TableCell>
<TableCell
sx={{
fontWeight: 600,
background: '#242C3D',
background: theme.palette.background.paper,
width: '200px',
}}
align="left"
@@ -0,0 +1,49 @@
import { ColumnsType } from '../../DetailTable';
export const EconomicsInfoColumns: ColumnsType[] = [
{
field: 'estimatedTotalReward',
title: 'Estimated Total Reward',
flex: 1,
headerAlign: 'left',
tooltipInfo: 'Estimated reward per epoch for this profit margin if your node is selected in the active set.',
},
{
field: 'estimatedOperatorReward',
title: 'Estimated Operator Reward',
flex: 1,
headerAlign: 'left',
tooltipInfo: 'Estimated reward per epoch for this profit margin if your node is selected in the active set.',
},
{
field: 'selectionChance',
title: 'Active Set Probability',
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.',
},
{
field: 'stakeSaturation',
title: 'Stake Saturation',
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is: 1 million NYM, computed as S/K where S is total amount of tokens available to stakeholders and K is the number of nodes in the reward set.',
},
{
field: 'profitMargin',
title: 'Profit Margin',
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Percentage of the delegates rewards that the operator takes as fee before rewards are distributed to the delegates.',
},
{
field: 'avgUptime',
title: 'Avg. Uptime',
flex: 1,
headerAlign: 'left',
tooltipInfo: 'Nodes average uptime in the last 24h.',
},
];
@@ -0,0 +1,31 @@
import * as React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { EconomicsProgress } from './EconomicsProgress';
export default {
title: 'Mix Node Detail/Economics/ProgressBar',
component: EconomicsProgress,
} as ComponentMeta<typeof EconomicsProgress>;
const Template: ComponentStory<typeof EconomicsProgress> = (args) => <EconomicsProgress {...args} />;
export const Empty = Template.bind({});
Empty.args = {};
export const OverThreshold = Template.bind({});
OverThreshold.args = {
threshold: 100,
value: 120,
};
export const UnderThreshold = Template.bind({});
UnderThreshold.args = {
threshold: 100,
value: 80,
};
export const OnThreshold = Template.bind({});
OnThreshold.args = {
threshold: 100,
value: 100,
};
@@ -0,0 +1,38 @@
import * as React from 'react';
import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress';
import { useTheme } from '@mui/material/styles';
import { Box } from '@mui/system';
const parseToNumber = (value: number | undefined | string) =>
typeof value === 'string' ? parseInt(value || '', 10) : value || 0;
export const EconomicsProgress: React.FC<
LinearProgressProps & {
threshold?: number;
}
> = ({ threshold, ...props }) => {
const theme = useTheme();
const { value } = props;
const valueNumber: number = parseToNumber(value);
const thresholdNumber: number = parseToNumber(threshold);
const percentageColor = valueNumber > (threshold || 100) ? 'warning' : 'inherit';
const percentageToDisplay = Math.min(valueNumber, thresholdNumber);
return (
<Box
sx={{
width: 6 / 10,
color: valueNumber > (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee,
}}
>
<LinearProgress
{...props}
variant="determinate"
color={percentageColor}
value={percentageToDisplay}
sx={{ width: '100%', borderRadius: '5px', backgroundColor: theme.palette.common.white }}
/>
</Box>
);
};
@@ -0,0 +1,129 @@
import * as React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { DelegatorsInfoTable } from './Table';
import { EconomicsInfoColumns } from './Columns';
import { EconomicsInfoRowWithIndex } from './types';
export default {
title: 'Mix Node Detail/Economics',
component: DelegatorsInfoTable,
} as ComponentMeta<typeof DelegatorsInfoTable>;
const row: EconomicsInfoRowWithIndex = {
id: 1,
selectionChance: {
value: 'High',
},
avgUptime: {
value: '65 %',
},
estimatedOperatorReward: {
value: '80000.123456 NYM',
},
estimatedTotalReward: {
value: '80000.123456 NYM',
},
profitMargin: {
value: '10 %',
},
stakeSaturation: {
value: '80 %',
progressBarValue: 80,
},
};
const rowVeryHighProbabilitySelection: EconomicsInfoRowWithIndex = {
...row,
selectionChance: {
value: 'Very High',
},
};
const rowModerateProbabilitySelection: EconomicsInfoRowWithIndex = {
...row,
selectionChance: {
value: 'Moderate',
},
};
const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = {
...row,
selectionChance: {
value: 'Low',
},
};
const rowVeryLowProbabilitySelection: EconomicsInfoRowWithIndex = {
...row,
selectionChance: {
value: 'Very Low',
},
};
const emptyRow: EconomicsInfoRowWithIndex = {
id: 1,
selectionChance: {
value: '-',
progressBarValue: 0,
},
avgUptime: {
value: '-',
},
estimatedOperatorReward: {
value: '-',
},
estimatedTotalReward: {
value: '-',
},
profitMargin: {
value: '-',
},
stakeSaturation: {
value: '-',
progressBarValue: 0,
},
};
const Template: ComponentStory<typeof DelegatorsInfoTable> = (args) => <DelegatorsInfoTable {...args} />;
export const Empty = Template.bind({});
Empty.args = {
rows: [emptyRow],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
export const selectionChanceVeryHigh = Template.bind({});
selectionChanceVeryHigh.args = {
rows: [rowVeryHighProbabilitySelection],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
export const selectionChanceHigh = Template.bind({});
selectionChanceHigh.args = {
rows: [row],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
export const selectionChanceModerate = Template.bind({});
selectionChanceModerate.args = {
rows: [rowModerateProbabilitySelection],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
export const selectionChanceLow = Template.bind({});
selectionChanceLow.args = {
rows: [rowLowProbabilitySelection],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
export const selectionChanceVeryLow = Template.bind({});
selectionChanceVeryLow.args = {
rows: [rowVeryLowProbabilitySelection],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
@@ -0,0 +1,55 @@
import { currencyToString } from '../../../utils/currency';
import { useMixnodeContext } from '../../../context/mixnode';
import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '../../../typeDefs/explorer-api';
import { EconomicsInfoRowWithIndex } from './types';
const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) => {
const inclusionProbability = economicDynamicsStats?.data?.active_set_inclusion_probability;
switch (inclusionProbability) {
case 'High':
case 'Moderate':
case 'Low':
return inclusionProbability;
case 'VeryHigh':
return 'Very High';
case 'VeryLow':
return 'Very Low';
default:
return '-';
}
};
export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
const { economicDynamicsStats, mixNode } = useMixnodeContext();
const estimatedNodeRewards =
currencyToString((economicDynamicsStats?.data?.estimated_total_node_reward || '').toString()) || '-';
const estimatedOperatorRewards =
currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-';
const stakeSaturation = economicDynamicsStats?.data?.stake_saturation || '-';
const profitMargin = mixNode?.data?.mix_node.profit_margin_percent || '-';
const avgUptime = economicDynamicsStats?.data?.current_interval_uptime;
return {
id: 1,
estimatedTotalReward: {
value: estimatedNodeRewards,
},
estimatedOperatorReward: {
value: estimatedOperatorRewards,
},
selectionChance: {
value: selectionChance(economicDynamicsStats),
},
stakeSaturation: {
progressBarValue: typeof stakeSaturation === 'number' ? stakeSaturation * 100 : 0,
value: typeof stakeSaturation === 'number' ? `${(stakeSaturation * 100).toFixed(2)} %` : '-',
},
profitMargin: {
value: profitMargin ? `${profitMargin} %` : '-',
},
avgUptime: {
value: avgUptime ? `${avgUptime} %` : '-',
},
};
};
@@ -0,0 +1,175 @@
import * as React from 'react';
import {
IconButton,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
useMediaQuery,
} from '@mui/material';
import { Box } from '@mui/system';
import { styled, useTheme, Theme } from '@mui/material/styles';
import Tooltip, { tooltipClasses, TooltipProps } from '@mui/material/Tooltip';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types';
import { EconomicsProgress } from './EconomicsProgress';
import { cellStyles } from '../../Universal-DataGrid';
import { UniversalTableProps } from '../../DetailTable';
const tooltipBackGroundColor = '#A0AED1';
const threshold = 100;
const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => {
const progressBarValue = value?.progressBarValue || 0;
const fieldValue = value.value;
if (progressBarValue > 100) {
return theme.palette.warning.main;
}
if (field === 'selectionChance') {
switch (fieldValue) {
case 'High':
case 'Very High':
return theme.palette.nym.networkExplorer.selectionChance.overModerate;
case 'Moderate':
return theme.palette.nym.networkExplorer.selectionChance.moderate;
case 'Low':
case 'Very Low':
return theme.palette.nym.networkExplorer.selectionChance.underModerate;
default:
return theme.palette.nym.wallet.fee;
}
}
return theme.palette.nym.wallet.fee;
};
const formatCellValues = (value: EconomicsRowsType, field: string, theme: Theme) => {
const isTablet = useMediaQuery(theme.breakpoints.down('lg'));
if (value.progressBarValue) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', flexDirection: isTablet ? 'column' : 'row' }} id="field">
<Typography
sx={{
mr: isTablet ? 0 : 1,
mb: isTablet ? 1 : 0,
fontWeight: '600',
fontSize: '12px',
}}
id={field}
>
{value.value}
</Typography>
<EconomicsProgress threshold={threshold} value={value.progressBarValue} />
</Box>
);
}
return (
<Box sx={{ display: 'flex', alignItems: 'center' }} id="field">
<Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}>
{value.value}
</Typography>
</Box>
);
};
export const DelegatorsInfoTable: React.FC<UniversalTableProps<EconomicsInfoRowWithIndex>> = ({
tableName,
columnsData,
rows,
}) => {
const theme = useTheme();
const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))({
[`& .${tooltipClasses.tooltip}`]: {
maxWidth: 230,
background: tooltipBackGroundColor,
color: theme.palette.nym.networkExplorer.nav.hover,
},
});
return (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
<TableHead>
<TableRow>
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{tooltipInfo && (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CustomTooltip
title={tooltipInfo}
id={field}
placement="top-start"
sx={{
'& .MuiTooltip-arrow': {
color: '#A0AED1',
},
}}
arrow
>
<IconButton
sx={{
padding: 0,
py: 1,
pr: 1,
}}
disableFocusRipple
disableRipple
>
<InfoOutlinedIcon
sx={{
height: '18px',
width: '18px',
}}
/>
</IconButton>
</CustomTooltip>
</Box>
)}
{title}
</Box>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows?.map((eachRow) => (
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
{columnsData?.map((_, index: number) => {
const { field } = columnsData[index];
const value: EconomicsRowsType = (eachRow as any)[field];
return (
<TableCell
key={_.title}
component="th"
scope="row"
variant="body"
sx={{
...cellStyles,
padding: 2,
width: 200,
fontSize: 12,
fontWeight: 600,
color: textColour(value, field, theme),
}}
data-testid={`${_.title.replace(/ /g, '-')}-value`}
>
{formatCellValues(value, columnsData[index].field, theme)}
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
@@ -0,0 +1,3 @@
export { DelegatorsInfoTable } from './Table';
export { EconomicsInfoColumns } from './Columns';
export { EconomicsInfoRows } from './Rows';
@@ -0,0 +1,15 @@
export type EconomicsRowsType = {
progressBarValue?: number;
value: string;
};
export interface EconomicsInfoRow {
estimatedTotalReward: EconomicsRowsType;
estimatedOperatorReward: EconomicsRowsType;
selectionChance: EconomicsRowsType;
stakeSaturation: EconomicsRowsType;
profitMargin: EconomicsRowsType;
avgUptime: EconomicsRowsType;
}
export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number };
@@ -11,6 +11,7 @@ export type MixnodeRowType = {
self_percentage: string;
host: string;
layer: string;
profit_percentage: string;
};
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
@@ -22,6 +23,7 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
const delegations = Number(item.total_delegation.amount) || 0;
const totalBond = pledge + delegations;
const selfPercentage = ((pledge * 100) / totalBond).toFixed(2);
const profitPercentage = item.mix_node.profit_margin_percent || 0;
return {
id: item.owner,
status: item.status,
@@ -32,5 +34,6 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
self_percentage: selfPercentage,
host: item?.mix_node?.host || '',
layer: item?.layer || '',
profit_percentage: `${profitPercentage}%`,
};
}
+6 -3
View File
@@ -22,9 +22,12 @@ export const Socials: React.FC<{ isFooter?: boolean }> = ({ isFooter }) => {
<IconButton component="a" href={TELEGRAM_LINK} target="_blank" data-testid="telegram">
<TelegramIcon color={color} size={24} />
</IconButton>
<IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord">
<DiscordIcon color={color} size={24} />
</IconButton>
{false && (
<IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord">
<DiscordIcon color={color} size={24} />
</IconButton>
)}
<IconButton component="a" href={TWITTER_LINK} target="_blank" data-testid="twitter">
<TwitterIcon color={color} size={24} />
</IconButton>
@@ -0,0 +1,15 @@
export type RowsType = {
value?: string | number;
visualProgressValue?: number;
};
export interface DelegatorsInfoRow {
estimated_total_reward: RowsType;
estimated_operator_reward: RowsType;
active_set_probability: RowsType;
stake_saturation: RowsType;
profit_margin: RowsType;
avg_uptime: RowsType;
}
export type DelegatorsInfoRowWithIndex = DelegatorsInfoRow & { id: number };
+20 -1
View File
@@ -3,6 +3,7 @@ import {
ApiState,
DelegationsResponse,
MixNodeDescriptionResponse,
MixNodeEconomicDynamicsStatsResponse,
MixNodeResponseItem,
StatsResponse,
StatusResponse,
@@ -18,6 +19,7 @@ import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../componen
interface MixnodeState {
delegations?: ApiState<DelegationsResponse>;
description?: ApiState<MixNodeDescriptionResponse>;
economicDynamicsStats?: ApiState<MixNodeEconomicDynamicsStatsResponse>;
mixNode?: ApiState<MixNodeResponseItem | undefined>;
mixNodeRow?: MixnodeRowType;
stats?: ApiState<StatsResponse>;
@@ -71,6 +73,13 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
'Failed to fetch mixnode description',
);
const [economicDynamicsStats, fetchEconomicDynamicsStats, clearEconomicDynamicsStats] =
useApiState<MixNodeEconomicDynamicsStatsResponse>(
mixNodeIdentityKey,
Api.fetchMixnodeEconomicDynamicsStatsById,
'Failed to fetch mixnode dynamics stats by id',
);
const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>(
mixNodeIdentityKey,
Api.fetchUptimeStoryById,
@@ -84,6 +93,7 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
clearStatus();
clearStats();
clearDescription();
clearEconomicDynamicsStats();
clearUptimeHistory();
// fetch the mixnode, then get all the other stuff
@@ -93,7 +103,14 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
return;
}
setMixnodeRow(mixNodeResponseItemToMixnodeRowType(value.data));
Promise.all([fetchDelegations(), fetchStatus(), fetchStats(), fetchDescription(), fetchUptimeHistory()]);
Promise.all([
fetchDelegations(),
fetchStatus(),
fetchStats(),
fetchDescription(),
fetchEconomicDynamicsStats(),
fetchUptimeHistory(),
]);
});
}, [mixNodeIdentityKey]);
@@ -103,6 +120,7 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
mixNode,
mixNodeRow,
description,
economicDynamicsStats,
stats,
status,
uptimeStory,
@@ -113,6 +131,7 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
mixNode,
mixNodeRow,
description,
economicDynamicsStats,
stats,
status,
uptimeStory,
@@ -3,6 +3,7 @@ import { Alert, AlertTitle, Box, CircularProgress, Grid, Typography } from '@mui
import { useParams } from 'react-router-dom';
import { ColumnsType, DetailTable } from '../../components/DetailTable';
import { BondBreakdownTable } from '../../components/MixNodes/BondBreakdown';
import { DelegatorsInfoTable, EconomicsInfoColumns, EconomicsInfoRows } from '../../components/MixNodes/Economics';
import { ComponentError } from '../../components/ComponentError';
import { ContentCard } from '../../components/ContentCard';
import { TwoColSmallTable } from '../../components/TwoColSmallTable';
@@ -82,6 +83,16 @@ const PageMixnodeDetailWithState: React.FC = () => {
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12}>
<DelegatorsInfoTable
columnsData={EconomicsInfoColumns}
tableName="Delegators info table"
rows={[EconomicsInfoRows()]}
/>
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12}>
<ContentCard title="Bond Breakdown">
+23 -5
View File
@@ -16,6 +16,7 @@ import { CustomColumnHeading } from '../../components/CustomColumnHeading';
import { Title } from '../../components/Title';
import { cellStyles, UniversalDataGrid } from '../../components/Universal-DataGrid';
import { currencyToString } from '../../utils/currency';
import { splice } from '../../utils';
import { getMixNodeStatusColor } from '../../components/MixNodes/Status';
import { MixNodeStatusDropdown } from '../../components/MixNodes/StatusDropdown';
@@ -85,7 +86,7 @@ export const PageMixnodes: React.FC = () => {
field: 'owner',
headerName: 'Owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 380,
width: 200,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
@@ -95,7 +96,7 @@ export const PageMixnodes: React.FC = () => {
sx={getCellStyles(theme, params.row)}
data-testid="big-dipper-link"
>
{params.value}
{splice(7, 29, params.value)}
</MuiLink>
),
},
@@ -104,7 +105,7 @@ export const PageMixnodes: React.FC = () => {
headerName: 'Identity Key',
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
headerClassName: 'MuiDataGrid-header-override',
width: 380,
width: 180,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<>
@@ -119,7 +120,7 @@ export const PageMixnodes: React.FC = () => {
to={`/network-components/mixnode/${params.value}`}
data-testid="identity-link"
>
{params.value}
{splice(7, 29, params.value)}
</MuiLink>
</>
),
@@ -130,7 +131,7 @@ export const PageMixnodes: React.FC = () => {
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
type: 'number',
headerClassName: 'MuiDataGrid-header-override',
width: 150,
width: 200,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
@@ -213,6 +214,23 @@ export const PageMixnodes: React.FC = () => {
</MuiLink>
),
},
{
field: 'profit_percentage',
headerName: 'Profit Margin',
renderHeader: () => <CustomColumnHeading headingTitle="Profit Margin" />,
headerClassName: 'MuiDataGrid-header-override',
width: 140,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
component={RRDLink}
to={`/network-components/mixnode/${params.row.identity_key}`}
>
{params.value}
</MuiLink>
),
},
];
const handlePageSize = (event: SelectChangeEvent<string>) => {
@@ -0,0 +1,7 @@
import { Meta } from '@storybook/addon-docs';
<Meta title="Introduction" />
# Nym Network Explorer Storybook
This is the Storybook for the Nym Network Explorer.
+10
View File
@@ -203,3 +203,13 @@ export type UptimeStoryResponse = {
identity: string;
owner: string;
};
export type MixNodeEconomicDynamicsStatsResponse = {
stake_saturation: number;
active_set_inclusion_probability: 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow';
reserve_set_inclusion_probability: 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow';
estimated_total_node_reward: number;
estimated_operator_reward: number;
estimated_delegators_reward: number;
current_interval_uptime: number;
};
+9
View File
@@ -36,3 +36,12 @@ export function countryDataToGridRow(countriesData: CountryData[]): CountryDataR
const sorted = formatted.sort((a, b) => (a.nodes < b.nodes ? 1 : -1));
return sorted;
}
export const splice = (start: number, deleteCount: number, address?: string): string => {
if (address) {
const array = address.split('');
array.splice(start, deleteCount, '...');
return array.join('');
}
return '';
};
+15
View File
@@ -46,6 +46,11 @@ export interface NymPaletteVariant {
standby: string;
};
};
selectionChance: {
overModerate: string;
moderate: string;
underModerate: string;
},
}
// -------------------------------------------------------------------------------------------------------------------
@@ -104,6 +109,11 @@ export const darkMode: NymPaletteVariant = {
standby: '#5FD7EF',
},
},
selectionChance: {
overModerate: '#20D073',
moderate: '#EBA53D',
underModerate: '#DA465B',
},
};
/**
@@ -132,6 +142,11 @@ export const lightMode: NymPaletteVariant = {
standby: '#55C1D7',
},
},
selectionChance: {
overModerate: '#20D073',
moderate: '#EBA53D',
underModerate: '#DA465B',
},
};
/**
@@ -40,6 +40,11 @@ export interface NetworkExplorerPalette {
inactive: string;
};
};
selectionChance: {
overModerate: string;
moderate: string;
underModerate: string;
}
};
}
@@ -84,6 +89,11 @@ export const networkExplorerPalette = (variant: NymPaletteVariant): NetworkExplo
inactive: variant.text.main,
},
},
selectionChance: {
overModerate: variant.selectionChance.overModerate,
moderate: variant.selectionChance.moderate,
underModerate: variant.selectionChance.underModerate,
}
},
});