diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/App.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/App.tsx index 55a30d407..cbd1f6a53 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/App.tsx +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/App.tsx @@ -8,7 +8,8 @@ import { NodeRewardsArgs, NodeRewardsResponse } from '../../declarations/trustwo import { NodeList } from './components/NodeList'; import Header from './components/Header'; import { dateToNanoseconds } from './utils/utils'; -import { NodeChart } from './components/NodePage'; +import { NodePage } from './components/NodePage'; +import { NodeProviderPage } from './components/NodeProviderPage'; // Theme configuration const darkTheme = createTheme({ @@ -51,7 +52,7 @@ const App: React.FC = () => { const [periodFilter, setPeriodFilter] = useState({ dateStart, dateEnd }); const [nodeRewards, setNodeRewards] = useState([]); const [subnets, setSubnets] = useState>(new Set()); - const [nodeProviders, setNodeProviders] = useState>(new Set()); + const [providers, setProviders] = useState>(new Set()); const [isLoading, setIsLoading] = useState(true); const [drawerOpen, setDrawerOpen] = useState(false); const theme = useTheme(); @@ -69,9 +70,11 @@ const App: React.FC = () => { const nodeRewardsResponse = await trustworthy_node_metrics.node_rewards(request); const sortedNodeRewards = nodeRewardsResponse.sort((a, b) => a.rewards_percent - b.rewards_percent); const subnets = new Set(sortedNodeRewards.flatMap(node => node.daily_node_metrics.map(data => data.subnet_assigned.toText()))); + const providers = new Set(sortedNodeRewards.flatMap(node => node.node_provider_id.toText())); setNodeRewards(sortedNodeRewards); setSubnets(subnets); + setProviders(providers); } catch (error) { console.error("Error fetching node:", error); } finally { @@ -84,12 +87,12 @@ const App: React.FC = () => { const drawerProps = useMemo(() => ({ subnets, - nodeProviders, + providers, drawerWidth, temporary: isSmallScreen, drawerOpen, onClosed: () => setDrawerOpen(false) - }), [subnets, nodeProviders, drawerWidth, isSmallScreen, drawerOpen]); + }), [subnets, providers, drawerWidth, isSmallScreen, drawerOpen]); return ( @@ -107,12 +110,16 @@ const App: React.FC = () => { isLoading ? : } /> : + isLoading ? : } /> : } /> + : + } /> diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/Drawer.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/Drawer.tsx index 679bd2e2e..2a8b5f2b0 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/Drawer.tsx +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/Drawer.tsx @@ -17,14 +17,14 @@ import { ExpandLess, ExpandMore } from '@mui/icons-material'; interface DrawerProps { subnets: Set; - nodeProviders: Set; + providers: Set; drawerWidth: number; temporary: boolean; drawerOpen: boolean; onClosed: () => void; } -const Drawer: React.FC = ({ subnets, nodeProviders, drawerWidth, temporary, drawerOpen, onClosed }) => { +const Drawer: React.FC = ({ subnets, providers, drawerWidth, temporary, drawerOpen, onClosed }) => { const [isSubnetsOpen, setIsSubnetsOpen] = React.useState(false); const [isNodeProvidersOpen, setIsNodeProvidersOpen] = React.useState(false); @@ -85,7 +85,7 @@ const Drawer: React.FC = ({ subnets, nodeProviders, drawerWidth, te {renderCollapsibleList("Subnets", subnets, isSubnetsOpen, setIsSubnetsOpen, "subnets")} - {renderCollapsibleList("Node Providers", nodeProviders, isNodeProvidersOpen, setIsNodeProvidersOpen, "node-providers")} + {renderCollapsibleList("Node Providers", providers, isNodeProvidersOpen, setIsNodeProvidersOpen, "providers")} ); diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/ExportTable.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/ExportTable.tsx new file mode 100644 index 000000000..7edb5db10 --- /dev/null +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/ExportTable.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { DataGrid, GridColDef, GridRowsProp, GridToolbarContainer, GridToolbarExport } from '@mui/x-data-grid'; + +function CustomToolbar() { + return ( + + + + ); +} + +interface ExportCustomToolbarProps { + colDef: GridColDef[]; + rows: GridRowsProp; +} + +export const ExportTable: React.FC = ({ colDef, rows }) => { + return ( +
+ +
+ ); +} diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeDailyData.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeDailyData.tsx deleted file mode 100644 index 2b33d8338..000000000 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeDailyData.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from 'react'; -import { DataGrid, GridColDef, GridRowsProp, GridToolbarContainer, GridToolbarExport } from '@mui/x-data-grid'; -import { ChartData } from '../utils/utils'; - -function CustomToolbar() { - return ( - - - - ); -} - -interface ExportCustomToolbarProps { - chartDailyData: ChartData[]; -} - -export const ExportCustomToolbar: React.FC = ({ chartDailyData }) => { - const rows: GridRowsProp = chartDailyData.map((dailyData, index) => { - return { - id: index + 1, - col1: dailyData.date.toLocaleDateString('en-GB'), - col2: dailyData.dailyNodeMetrics?.num_blocks_proposed, - col3: dailyData.dailyNodeMetrics?.num_blocks_failed, - col4: dailyData.dailyNodeMetrics?.subnet_assigned, - }; - }); - - const columns: GridColDef[] = [ - { field: 'col1', headerName: 'Date', width: 200 }, - { field: 'col2', headerName: 'Blocks Proposed', width: 150 }, - { field: 'col3', headerName: 'Blocks Failed', width: 150 }, - { field: 'col4', headerName: 'Subnet Assigned', width: 550 }, - ]; - return ( -
- -
- ); -} diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeInfo.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeInfo.tsx index 2311b46a5..db8648ca5 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeInfo.tsx +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeInfo.tsx @@ -1,29 +1,22 @@ import React from 'react'; import Typography from '@mui/material/Typography'; -interface NodeInfoProps { - nodeId: string; - nodeProviderId: string; +interface InfoFormatterProps { + name: string; + value: string; } -const NodeInfo: React.FC = ({ nodeId, nodeProviderId }) => { +const InfoFormatter: React.FC = ({ name, value }) => { return (
- {"Node ID"} + {name} - {nodeId} - - - - {"Node Provider ID"} - - - {nodeProviderId} + {value}
); }; -export default NodeInfo; +export default InfoFormatter; diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodePage.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodePage.tsx index ba10254b7..58bfc2d71 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodePage.tsx +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodePage.tsx @@ -1,17 +1,18 @@ import React from 'react'; -import { ChartData, generateChartData } from '../utils/utils'; +import { ChartData, formatDateToUTC, generateChartData } from '../utils/utils'; import { WidgetGauge, WidgetNumber } from './Widgets'; import { PeriodFilter } from './FilterBar'; import { Box, Divider, Grid, Paper, Typography } from '@mui/material'; import { useParams } from 'react-router-dom'; import DailyPerformanceChart from './DailyPerformanceChart'; -import NodeInfo from './NodeInfo'; import { paperStyle, boxStyleWidget } from '../Styles'; import { NodeRewardsResponse } from '../../../declarations/trustworthy-node-metrics/trustworthy-node-metrics.did'; -import RewardsInfo, { LinearReductionChart } from './RewardsInfo'; -import { ExportCustomToolbar } from './NodeDailyData'; +import RewardsInfo from './RewardsInfo'; +import { ExportTable } from './ExportTable'; +import InfoFormatter from './NodeInfo'; +import { GridColDef, GridRowsProp } from '@mui/x-data-grid'; -export interface NodeChartProps { +export interface NodePageProps { nodeRewards: NodeRewardsResponse[]; periodFilter: PeriodFilter; } @@ -30,7 +31,7 @@ const NodePerformanceStats: React.FC<{ rewardsReduction: string }> = ({ rewardsR ); -export const NodeChart: React.FC = ({ nodeRewards, periodFilter }) => { +export const NodePage: React.FC = ({ nodeRewards, periodFilter }) => { const { node } = useParams(); const nodeMetrics = nodeRewards.find((metrics) => metrics.node_id.toText() === node); @@ -43,6 +44,27 @@ export const NodeChart: React.FC = ({ nodeRewards, periodFilter const rewardsPercent = Math.round(nodeMetrics.rewards_percent * 100); const rewardsReduction = 100 - rewardsPercent; + let index = 0; + const rows: GridRowsProp = nodeMetrics.daily_node_metrics.map((data) => { + index = index + 1; + return { + id: index, + col1: new Date(Number(data.ts) / 1000000), + col2: data.num_blocks_proposed, + col3: data.num_blocks_failed, + col4: data.failure_rate, + col5: data.subnet_assigned, + }; + }); + + const colDef: GridColDef[] = [ + { field: 'col1', headerName: 'Date (UTC)', width: 200, valueFormatter: (value: Date) => formatDateToUTC(value)}, + { field: 'col2', headerName: 'Blocks Proposed', width: 150 }, + { field: 'col3', headerName: 'Blocks Failed', width: 150 }, + { field: 'col4', headerName: 'Daily Failure Rate', width: 350 , valueFormatter: (value: number) => `${value * 100}%`,}, + { field: 'col5', headerName: 'Subnet Assigned', width: 550 }, + ]; + return ( @@ -54,7 +76,8 @@ export const NodeChart: React.FC = ({ nodeRewards, periodFilter - + + @@ -72,7 +95,7 @@ export const NodeChart: React.FC = ({ nodeRewards, periodFilter - + @@ -80,4 +103,4 @@ export const NodeChart: React.FC = ({ nodeRewards, periodFilter ); }; -export default NodeChart; +export default NodePage; diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeProviderPage.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeProviderPage.tsx new file mode 100644 index 000000000..052981da0 --- /dev/null +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeProviderPage.tsx @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; +import { Box, Grid, Paper, Typography } from '@mui/material'; +import { axisClasses, BarChart, StackOrderType } from '@mui/x-charts'; +import Divider from '@mui/material/Divider'; +import { useParams } from 'react-router-dom'; +import { formatDateToUTC, generateChartData, getFormattedDates } from '../utils/utils'; +import { PeriodFilter } from './FilterBar'; +import { Root } from './NodeList'; +import { NodeRewardsResponse } from '../../../declarations/trustworthy-node-metrics/trustworthy-node-metrics.did'; +import { paperStyle } from '../Styles'; +import InfoFormatter from './NodeInfo'; +import { ExportTable } from './ExportTable'; +import { GridColDef, GridRowsProp } from '@mui/x-data-grid'; + +export interface NodeProviderPageProps { + nodeRewards: NodeRewardsResponse[], + periodFilter: PeriodFilter + } + +export const NodeProviderPage: React.FC = ({ nodeRewards, periodFilter }) => { + const { provider } = useParams(); + const providerNodeMetrics = nodeRewards + .filter((nodeMetrics) => nodeMetrics.node_provider_id.toText() === provider) + const highFailureRateChart = providerNodeMetrics + .filter(nodeMetrics => nodeMetrics.rewards_stats.rewards_reduction > 0) + .flatMap(nodeMetrics => { + const chartData = generateChartData(periodFilter, nodeMetrics.daily_node_metrics); + return { + data: chartData.map(data => data.dailyNodeMetrics? data.dailyNodeMetrics.failure_rate * 100: null), + label: nodeMetrics.node_id.toText(), + stack: 'total' + } + }); + + let index = 0; + const rows: GridRowsProp = providerNodeMetrics.flatMap((nodeRewards) => { + return nodeRewards.daily_node_metrics.map((data) => { + index = index + 1; + return { + id: index, + col1: new Date(Number(data.ts) / 1000000), + col2: nodeRewards.node_id, + col3: data.num_blocks_proposed, + col4: data.num_blocks_failed, + col5: data.failure_rate, + col6: data.subnet_assigned, + }; + }) + }); + + const colDef: GridColDef[] = [ + { field: 'col1', headerName: 'Date (UTC)', width: 200, valueFormatter: (value: Date) => formatDateToUTC(value)}, + { field: 'col2', headerName: 'Node ID', width: 550 }, + { field: 'col3', headerName: 'Blocks Proposed', width: 150 }, + { field: 'col4', headerName: 'Blocks Failed', width: 150 }, + { field: 'col5', headerName: 'Daily Failure Rate', width: 350 , valueFormatter: (value: number) => `${value * 100}%`,}, + { field: 'col6', headerName: 'Subnet Assigned', width: 550 }, + ]; + + return ( + + + + + + + {"Node Provider"} + + + + + + + + + Daily Failure Rate + + + For nodes with rewards reduction + + + + `${value}%`, + }]} + leftAxis={null} + borderRadius={9} + series={highFailureRateChart} + height={300} + /> + + + + + + + + ); +}; diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/RewardsInfo.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/RewardsInfo.tsx index 76912919a..8086b3bcd 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/RewardsInfo.tsx +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/RewardsInfo.tsx @@ -7,20 +7,23 @@ import { axisClasses, ChartsReferenceLine, LineChart } from '@mui/x-charts'; const NodeRewardExplanation: React.FC<{ failureRate: number; rewardReduction: number }> = ({ failureRate, rewardReduction }) => { return ( + {/* Title Section */} How are rewards computed? - + + {/* Node Unassigned Section */} + Node Unassigned: When a node is not assigned to any subnet, it automatically receives the full reward (100%). No further calculations are needed. - - {/* Assigned Node */} + + {/* Node Assigned Section */} Node Assigned: @@ -42,9 +45,8 @@ const NodeRewardExplanation: React.FC<{ failureRate: number; rewardReduction: nu - - + {/* Failure Rate Calculation */} @@ -61,9 +63,12 @@ const NodeRewardExplanation: React.FC<{ failureRate: number; rewardReduction: nu + - {/* Apply Linear Reduction Function */} - + {/* Reward Reduction Section */} + + {/* Linear Reduction Function */} + Apply Linear Reduction Function: @@ -71,19 +76,47 @@ const NodeRewardExplanation: React.FC<{ failureRate: number; rewardReduction: nu Based on the failure rate, we apply a linear reduction function to determine how much the failure rate reduces the node's rewards. + + {/* Specific Failure Rate Conditions */} + + + + Failure Rates Below 10%: For failure rates ≤ 10%, there is no reduction in rewards. The rewards reduction is 0%, meaning for performance below this threshold, rewards remain unaffected. + + + + + Failure Rates Above 70%: Once the failure rate exceeds 70%, the rewards reduction reaches its maximum of 100%. Any failure rate beyond this threshold results in a complete loss of rewards. + + + + - The final reward percentage is computed by subtracting the rewards reduction from 100%. + The final reward percentage for the assigned period is computed by subtracting the rewards reduction from 100%. + + + + {/* Total Rewards Calculation Placeholder */} + + + Compute total rewards: + + + Work in progress... - - + + {/* Reward Reduction Chart */} + + ); }; + export default NodeRewardExplanation; export const LinearReductionChart: React.FC<{ failureRate: number; rewardReduction: number }> = ({ failureRate, rewardReduction }) => { @@ -107,7 +140,7 @@ export const LinearReductionChart: React.FC<{ failureRate: number; rewardReducti Linear Rewards Reduction { return dates; }; - export const computeAverageFailureRate = (data: number[]): number => { - if (data.length === 0) return 0; - const sum = data.reduce((acc, val) => acc + val, 0); - return sum / data.length; - }; +export const formatDateToUTC = (date: Date): string => { + const day = String(date.getUTCDate()).padStart(2, '0'); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); // Months are zero-indexed + const year = date.getUTCFullYear(); + return `${day}-${month}-${year}`; + }; + +export const computeAverageFailureRate = (data: number[]): number => { + if (data.length === 0) return 0; + const sum = data.reduce((acc, val) => acc + val, 0); + return sum / data.length; + }; +