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:
@@ -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 }}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'@nymproject/eslint-config-react-typescript'
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts'],
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"@nymproject/eslint-config-react-typescript"
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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
@@ -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": [
|
||||
|
||||
@@ -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> =>
|
||||
|
||||
@@ -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
|
||||
</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: 'Node’s 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}%`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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">
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 '';
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user