diff --git a/.github/workflows/network-explorer.yml b/.github/workflows/network-explorer.yml
index 447cec41ec..742a70c1b8 100644
--- a/.github/workflows/network-explorer.yml
+++ b/.github/workflows/network-explorer.yml
@@ -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 }}"
diff --git a/.github/workflows/support-files/network-explorer/templates/success b/.github/workflows/support-files/network-explorer/templates/success
index 7e7d02dd64..1f380b79b9 100644
--- a/.github/workflows/support-files/network-explorer/templates/success
+++ b/.github/workflows/support-files/network-explorer/templates/success
@@ -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 }}
diff --git a/explorer/.eslintrc.js b/explorer/.eslintrc.js
new file mode 100644
index 0000000000..5feba99084
--- /dev/null
+++ b/explorer/.eslintrc.js
@@ -0,0 +1,14 @@
+module.exports = {
+ extends: [
+ '@nymproject/eslint-config-react-typescript'
+ ],
+ overrides: [
+ {
+ files: ['*.ts'],
+ parserOptions: {
+ project: 'tsconfig.json',
+ tsconfigRootDir: __dirname,
+ }
+ }
+ ]
+}
diff --git a/explorer/.eslintrc.json b/explorer/.eslintrc.json
deleted file mode 100644
index 7f84f4b42a..0000000000
--- a/explorer/.eslintrc.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "extends": [
- "@nymproject/eslint-config-react-typescript"
- ]
-}
diff --git a/explorer/.storybook/main.js b/explorer/.storybook/main.js
new file mode 100644
index 0000000000..1c2a975321
--- /dev/null
+++ b/explorer/.storybook/main.js
@@ -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,
+ },
+};
diff --git a/explorer/.storybook/preview.js b/explorer/.storybook/preview.js
new file mode 100644
index 0000000000..5d97f3b5ab
--- /dev/null
+++ b/explorer/.storybook/preview.js
@@ -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) => (
+
+
+
+ theme.palette.background.default,
+ color: (theme) => theme.palette.text.primary,
+ }}
+ >
+
+
+
+ Light mode
+
+
+
+
+
+ theme.palette.background.default,
+ color: (theme) => theme.palette.text.primary,
+ }}
+ >
+
+
+
+ Dark mode
+
+
+
+
+);
+
+export const decorators = [withThemeProvider];
diff --git a/explorer/package.json b/explorer/package.json
index 053022471d..95207ba309 100644
--- a/explorer/package.json
+++ b/explorer/package.json
@@ -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": [
diff --git a/explorer/src/api/index.ts b/explorer/src/api/index.ts
index d0949d1796..8c0c087f46 100644
--- a/explorer/src/api/index.ts
+++ b/explorer/src/api/index.ts
@@ -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 =>
(await fetch(`${MIXNODE_API}/${id}/description`)).json();
+ static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise =>
+ (await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json();
+
static fetchStatusById = async (id: string): Promise => (await fetch(`${MIXNODE_PING}/${id}`)).json();
static fetchUptimeStoryById = async (id: string): Promise =>
diff --git a/explorer/src/components/DetailTable.tsx b/explorer/src/components/DetailTable.tsx
index 9dc7d34c74..81bd6982f1 100644
--- a/explorer/src/components/DetailTable.tsx
+++ b/explorer/src/components/DetailTable.tsx
@@ -12,12 +12,13 @@ export type ColumnsType = {
headerAlign: string;
flex?: number;
width?: number;
+ tooltipInfo?: string;
};
-export interface UniversalTableProps {
+export interface UniversalTableProps {
tableName: string;
columnsData: ColumnsType[];
- rows: any[];
+ rows: T[];
}
function formatCellValues(val: string | number, field: string) {
diff --git a/explorer/src/components/MixNodes/BondBreakdown.tsx b/explorer/src/components/MixNodes/BondBreakdown.tsx
index 4332b182e1..90789ab9bf 100644
--- a/explorer/src/components/MixNodes/BondBreakdown.tsx
+++ b/explorer/src/components/MixNodes/BondBreakdown.tsx
@@ -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,
}}
>
+
+
+ Delegations
+
+
+ {`(${delegations?.data?.length} delegators)`}
+
+
-
+
Delegators
-
+
Stake
;
+
+const Template: ComponentStory = (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,
+};
diff --git a/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx b/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx
new file mode 100644
index 0000000000..1779d8d18f
--- /dev/null
+++ b/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx
@@ -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 (
+ (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee,
+ }}
+ >
+
+
+ );
+};
diff --git a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx
new file mode 100644
index 0000000000..be9c4c94f0
--- /dev/null
+++ b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx
@@ -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;
+
+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 = (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',
+};
diff --git a/explorer/src/components/MixNodes/Economics/Rows.ts b/explorer/src/components/MixNodes/Economics/Rows.ts
new file mode 100644
index 0000000000..0a620318d7
--- /dev/null
+++ b/explorer/src/components/MixNodes/Economics/Rows.ts
@@ -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 | 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} %` : '-',
+ },
+ };
+};
diff --git a/explorer/src/components/MixNodes/Economics/Table.tsx b/explorer/src/components/MixNodes/Economics/Table.tsx
new file mode 100644
index 0000000000..9c3f1ab2b4
--- /dev/null
+++ b/explorer/src/components/MixNodes/Economics/Table.tsx
@@ -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 (
+
+
+ {value.value}
+
+
+
+ );
+ }
+ return (
+
+
+ {value.value}
+
+
+ );
+};
+
+export const DelegatorsInfoTable: React.FC> = ({
+ tableName,
+ columnsData,
+ rows,
+}) => {
+ const theme = useTheme();
+
+ const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
+
+ ))({
+ [`& .${tooltipClasses.tooltip}`]: {
+ maxWidth: 230,
+ background: tooltipBackGroundColor,
+ color: theme.palette.nym.networkExplorer.nav.hover,
+ },
+ });
+
+ return (
+
+
+
+
+ {columnsData?.map(({ field, title, flex, tooltipInfo }) => (
+
+
+ {tooltipInfo && (
+
+
+
+
+
+
+
+ )}
+ {title}
+
+
+ ))}
+
+
+
+ {rows?.map((eachRow) => (
+
+ {columnsData?.map((_, index: number) => {
+ const { field } = columnsData[index];
+ const value: EconomicsRowsType = (eachRow as any)[field];
+
+ return (
+
+ {formatCellValues(value, columnsData[index].field, theme)}
+
+ );
+ })}
+
+ ))}
+
+
+
+ );
+};
diff --git a/explorer/src/components/MixNodes/Economics/index.ts b/explorer/src/components/MixNodes/Economics/index.ts
new file mode 100644
index 0000000000..6dec752246
--- /dev/null
+++ b/explorer/src/components/MixNodes/Economics/index.ts
@@ -0,0 +1,3 @@
+export { DelegatorsInfoTable } from './Table';
+export { EconomicsInfoColumns } from './Columns';
+export { EconomicsInfoRows } from './Rows';
diff --git a/explorer/src/components/MixNodes/Economics/types.ts b/explorer/src/components/MixNodes/Economics/types.ts
new file mode 100644
index 0000000000..b82b35f139
--- /dev/null
+++ b/explorer/src/components/MixNodes/Economics/types.ts
@@ -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 };
diff --git a/explorer/src/components/MixNodes/index.ts b/explorer/src/components/MixNodes/index.ts
index 2745785d41..d803c2e5ea 100644
--- a/explorer/src/components/MixNodes/index.ts
+++ b/explorer/src/components/MixNodes/index.ts
@@ -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}%`,
};
}
diff --git a/explorer/src/components/Socials.tsx b/explorer/src/components/Socials.tsx
index f84aa7bcc3..ded74ffe98 100644
--- a/explorer/src/components/Socials.tsx
+++ b/explorer/src/components/Socials.tsx
@@ -22,9 +22,12 @@ export const Socials: React.FC<{ isFooter?: boolean }> = ({ isFooter }) => {
-
-
-
+ {false && (
+
+
+
+ )}
+
diff --git a/explorer/src/components/delegatorsInfo/types.ts b/explorer/src/components/delegatorsInfo/types.ts
new file mode 100644
index 0000000000..b0e3fd6e99
--- /dev/null
+++ b/explorer/src/components/delegatorsInfo/types.ts
@@ -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 };
diff --git a/explorer/src/context/mixnode.tsx b/explorer/src/context/mixnode.tsx
index c71036e61d..c84ee60517 100644
--- a/explorer/src/context/mixnode.tsx
+++ b/explorer/src/context/mixnode.tsx
@@ -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;
description?: ApiState;
+ economicDynamicsStats?: ApiState;
mixNode?: ApiState;
mixNodeRow?: MixnodeRowType;
stats?: ApiState;
@@ -71,6 +73,13 @@ export const MixnodeContextProvider: React.FC = ({
'Failed to fetch mixnode description',
);
+ const [economicDynamicsStats, fetchEconomicDynamicsStats, clearEconomicDynamicsStats] =
+ useApiState(
+ mixNodeIdentityKey,
+ Api.fetchMixnodeEconomicDynamicsStatsById,
+ 'Failed to fetch mixnode dynamics stats by id',
+ );
+
const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState(
mixNodeIdentityKey,
Api.fetchUptimeStoryById,
@@ -84,6 +93,7 @@ export const MixnodeContextProvider: React.FC = ({
clearStatus();
clearStats();
clearDescription();
+ clearEconomicDynamicsStats();
clearUptimeHistory();
// fetch the mixnode, then get all the other stuff
@@ -93,7 +103,14 @@ export const MixnodeContextProvider: React.FC = ({
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 = ({
mixNode,
mixNodeRow,
description,
+ economicDynamicsStats,
stats,
status,
uptimeStory,
@@ -113,6 +131,7 @@ export const MixnodeContextProvider: React.FC = ({
mixNode,
mixNodeRow,
description,
+ economicDynamicsStats,
stats,
status,
uptimeStory,
diff --git a/explorer/src/pages/MixnodeDetail/index.tsx b/explorer/src/pages/MixnodeDetail/index.tsx
index a9fd29dbee..50cba3c790 100644
--- a/explorer/src/pages/MixnodeDetail/index.tsx
+++ b/explorer/src/pages/MixnodeDetail/index.tsx
@@ -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 = () => {
+
+
+
+
+
+
diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx
index e2919e430b..1386aa38cc 100644
--- a/explorer/src/pages/Mixnodes/index.tsx
+++ b/explorer/src/pages/Mixnodes/index.tsx
@@ -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: () => ,
- 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)}
),
},
@@ -104,7 +105,7 @@ export const PageMixnodes: React.FC = () => {
headerName: 'Identity Key',
renderHeader: () => ,
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)}
>
),
@@ -130,7 +131,7 @@ export const PageMixnodes: React.FC = () => {
renderHeader: () => ,
type: 'number',
headerClassName: 'MuiDataGrid-header-override',
- width: 150,
+ width: 200,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
{
),
},
+ {
+ field: 'profit_percentage',
+ headerName: 'Profit Margin',
+ renderHeader: () => ,
+ headerClassName: 'MuiDataGrid-header-override',
+ width: 140,
+ headerAlign: 'left',
+ renderCell: (params: GridRenderCellParams) => (
+
+ {params.value}
+
+ ),
+ },
];
const handlePageSize = (event: SelectChangeEvent) => {
diff --git a/explorer/src/stories/Introduction.stories.mdx b/explorer/src/stories/Introduction.stories.mdx
new file mode 100644
index 0000000000..dafab13de0
--- /dev/null
+++ b/explorer/src/stories/Introduction.stories.mdx
@@ -0,0 +1,7 @@
+import { Meta } from '@storybook/addon-docs';
+
+
+
+# Nym Network Explorer Storybook
+
+This is the Storybook for the Nym Network Explorer.
diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts
index 482db72596..9285f78b42 100644
--- a/explorer/src/typeDefs/explorer-api.ts
+++ b/explorer/src/typeDefs/explorer-api.ts
@@ -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;
+};
diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts
index f7e0768cbf..a44515b10c 100644
--- a/explorer/src/utils/index.ts
+++ b/explorer/src/utils/index.ts
@@ -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 '';
+};
diff --git a/ts-packages/mui-theme/src/theme/common.ts b/ts-packages/mui-theme/src/theme/common.ts
index e460aeee50..798991863d 100644
--- a/ts-packages/mui-theme/src/theme/common.ts
+++ b/ts-packages/mui-theme/src/theme/common.ts
@@ -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',
+ },
};
/**
diff --git a/ts-packages/mui-theme/src/theme/network-explorer.ts b/ts-packages/mui-theme/src/theme/network-explorer.ts
index 55f624cd71..f633b8ccf9 100644
--- a/ts-packages/mui-theme/src/theme/network-explorer.ts
+++ b/ts-packages/mui-theme/src/theme/network-explorer.ts
@@ -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,
+ }
},
});