Skip to content

Commit 0c411a7

Browse files
Add Node Provider Page (#813)
Co-authored-by: sa-github-api <138766536+sa-github-api@users.noreply.github.com>
1 parent 57440f4 commit 0c411a7

File tree

9 files changed

+245
-92
lines changed

9 files changed

+245
-92
lines changed

rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/App.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { NodeRewardsArgs, NodeRewardsResponse } from '../../declarations/trustwo
88
import { NodeList } from './components/NodeList';
99
import Header from './components/Header';
1010
import { dateToNanoseconds } from './utils/utils';
11-
import { NodeChart } from './components/NodePage';
11+
import { NodePage } from './components/NodePage';
12+
import { NodeProviderPage } from './components/NodeProviderPage';
1213

1314
// Theme configuration
1415
const darkTheme = createTheme({
@@ -51,7 +52,7 @@ const App: React.FC = () => {
5152
const [periodFilter, setPeriodFilter] = useState<PeriodFilter>({ dateStart, dateEnd });
5253
const [nodeRewards, setNodeRewards] = useState<NodeRewardsResponse[]>([]);
5354
const [subnets, setSubnets] = useState<Set<string>>(new Set());
54-
const [nodeProviders, setNodeProviders] = useState<Set<string>>(new Set());
55+
const [providers, setProviders] = useState<Set<string>>(new Set());
5556
const [isLoading, setIsLoading] = useState(true);
5657
const [drawerOpen, setDrawerOpen] = useState(false);
5758
const theme = useTheme();
@@ -69,9 +70,11 @@ const App: React.FC = () => {
6970
const nodeRewardsResponse = await trustworthy_node_metrics.node_rewards(request);
7071
const sortedNodeRewards = nodeRewardsResponse.sort((a, b) => a.rewards_percent - b.rewards_percent);
7172
const subnets = new Set(sortedNodeRewards.flatMap(node => node.daily_node_metrics.map(data => data.subnet_assigned.toText())));
73+
const providers = new Set(sortedNodeRewards.flatMap(node => node.node_provider_id.toText()));
7274

7375
setNodeRewards(sortedNodeRewards);
7476
setSubnets(subnets);
77+
setProviders(providers);
7578
} catch (error) {
7679
console.error("Error fetching node:", error);
7780
} finally {
@@ -84,12 +87,12 @@ const App: React.FC = () => {
8487

8588
const drawerProps = useMemo(() => ({
8689
subnets,
87-
nodeProviders,
90+
providers,
8891
drawerWidth,
8992
temporary: isSmallScreen,
9093
drawerOpen,
9194
onClosed: () => setDrawerOpen(false)
92-
}), [subnets, nodeProviders, drawerWidth, isSmallScreen, drawerOpen]);
95+
}), [subnets, providers, drawerWidth, isSmallScreen, drawerOpen]);
9396

9497
return (
9598
<ThemeProvider theme={darkTheme}>
@@ -107,12 +110,16 @@ const App: React.FC = () => {
107110
isLoading ? <LoadingIndicator /> : <NodeList nodeRewards={nodeRewards} periodFilter={periodFilter} />
108111
} />
109112
<Route path="/nodes/:node" element={
110-
isLoading ? <LoadingIndicator /> : <NodeChart nodeRewards={nodeRewards} periodFilter={periodFilter} />
113+
isLoading ? <LoadingIndicator /> : <NodePage nodeRewards={nodeRewards} periodFilter={periodFilter} />
111114
} />
112115
<Route path="/subnets/:subnet" element={
113116
// TODO: Add subnet page
114117
isLoading ? <LoadingIndicator /> : <NodeList nodeRewards={nodeRewards} periodFilter={periodFilter} />
115118
} />
119+
<Route path="/providers/:provider" element={
120+
// TODO: Add subnet page
121+
isLoading ? <LoadingIndicator /> : <NodeProviderPage nodeRewards={nodeRewards} periodFilter={periodFilter} />
122+
} />
116123
</Routes>
117124
</Box>
118125
</Box>

rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/Drawer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ import { ExpandLess, ExpandMore } from '@mui/icons-material';
1717

1818
interface DrawerProps {
1919
subnets: Set<string>;
20-
nodeProviders: Set<string>;
20+
providers: Set<string>;
2121
drawerWidth: number;
2222
temporary: boolean;
2323
drawerOpen: boolean;
2424
onClosed: () => void;
2525
}
2626

27-
const Drawer: React.FC<DrawerProps> = ({ subnets, nodeProviders, drawerWidth, temporary, drawerOpen, onClosed }) => {
27+
const Drawer: React.FC<DrawerProps> = ({ subnets, providers, drawerWidth, temporary, drawerOpen, onClosed }) => {
2828
const [isSubnetsOpen, setIsSubnetsOpen] = React.useState(false);
2929
const [isNodeProvidersOpen, setIsNodeProvidersOpen] = React.useState(false);
3030

@@ -85,7 +85,7 @@ const Drawer: React.FC<DrawerProps> = ({ subnets, nodeProviders, drawerWidth, te
8585
</ListItemButton>
8686
</Link>
8787
{renderCollapsibleList("Subnets", subnets, isSubnetsOpen, setIsSubnetsOpen, "subnets")}
88-
{renderCollapsibleList("Node Providers", nodeProviders, isNodeProvidersOpen, setIsNodeProvidersOpen, "node-providers")}
88+
{renderCollapsibleList("Node Providers", providers, isNodeProvidersOpen, setIsNodeProvidersOpen, "providers")}
8989
</List>
9090
</MUIDrawer>
9191
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from 'react';
2+
import { DataGrid, GridColDef, GridRowsProp, GridToolbarContainer, GridToolbarExport } from '@mui/x-data-grid';
3+
4+
function CustomToolbar() {
5+
return (
6+
<GridToolbarContainer>
7+
<GridToolbarExport />
8+
</GridToolbarContainer>
9+
);
10+
}
11+
12+
interface ExportCustomToolbarProps {
13+
colDef: GridColDef[];
14+
rows: GridRowsProp;
15+
}
16+
17+
export const ExportTable: React.FC<ExportCustomToolbarProps> = ({ colDef, rows }) => {
18+
return (
19+
<div style={{ height: 1000, width: '100%' }}>
20+
<DataGrid
21+
rows={rows}
22+
columns={colDef}
23+
slots={{
24+
toolbar: CustomToolbar,
25+
}}
26+
/>
27+
</div>
28+
);
29+
}

rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeDailyData.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,22 @@
11
import React from 'react';
22
import Typography from '@mui/material/Typography';
33

4-
interface NodeInfoProps {
5-
nodeId: string;
6-
nodeProviderId: string;
4+
interface InfoFormatterProps {
5+
name: string;
6+
value: string;
77
}
88

9-
const NodeInfo: React.FC<NodeInfoProps> = ({ nodeId, nodeProviderId }) => {
9+
const InfoFormatter: React.FC<InfoFormatterProps> = ({ name, value }) => {
1010
return (
1111
<div>
1212
<Typography gutterBottom variant="subtitle1" component="div">
13-
{"Node ID"}
13+
{name}
1414
</Typography>
1515
<Typography gutterBottom variant="subtitle2" sx={{ color: 'text.disabled' }} component="div">
16-
{nodeId}
17-
</Typography>
18-
19-
<Typography gutterBottom variant="subtitle1" component="div">
20-
{"Node Provider ID"}
21-
</Typography>
22-
<Typography gutterBottom variant="subtitle2" sx={{ color: 'text.disabled' }} component="div">
23-
{nodeProviderId}
16+
{value}
2417
</Typography>
2518
</div>
2619
);
2720
};
2821

29-
export default NodeInfo;
22+
export default InfoFormatter;

rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodePage.tsx

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import React from 'react';
2-
import { ChartData, generateChartData } from '../utils/utils';
2+
import { ChartData, formatDateToUTC, generateChartData } from '../utils/utils';
33
import { WidgetGauge, WidgetNumber } from './Widgets';
44
import { PeriodFilter } from './FilterBar';
55
import { Box, Divider, Grid, Paper, Typography } from '@mui/material';
66
import { useParams } from 'react-router-dom';
77
import DailyPerformanceChart from './DailyPerformanceChart';
8-
import NodeInfo from './NodeInfo';
98
import { paperStyle, boxStyleWidget } from '../Styles';
109
import { NodeRewardsResponse } from '../../../declarations/trustworthy-node-metrics/trustworthy-node-metrics.did';
11-
import RewardsInfo, { LinearReductionChart } from './RewardsInfo';
12-
import { ExportCustomToolbar } from './NodeDailyData';
10+
import RewardsInfo from './RewardsInfo';
11+
import { ExportTable } from './ExportTable';
12+
import InfoFormatter from './NodeInfo';
13+
import { GridColDef, GridRowsProp } from '@mui/x-data-grid';
1314

14-
export interface NodeChartProps {
15+
export interface NodePageProps {
1516
nodeRewards: NodeRewardsResponse[];
1617
periodFilter: PeriodFilter;
1718
}
@@ -30,7 +31,7 @@ const NodePerformanceStats: React.FC<{ rewardsReduction: string }> = ({ rewardsR
3031
</Box>
3132
);
3233

33-
export const NodeChart: React.FC<NodeChartProps> = ({ nodeRewards, periodFilter }) => {
34+
export const NodePage: React.FC<NodePageProps> = ({ nodeRewards, periodFilter }) => {
3435
const { node } = useParams();
3536

3637
const nodeMetrics = nodeRewards.find((metrics) => metrics.node_id.toText() === node);
@@ -43,6 +44,27 @@ export const NodeChart: React.FC<NodeChartProps> = ({ nodeRewards, periodFilter
4344
const rewardsPercent = Math.round(nodeMetrics.rewards_percent * 100);
4445
const rewardsReduction = 100 - rewardsPercent;
4546

47+
let index = 0;
48+
const rows: GridRowsProp = nodeMetrics.daily_node_metrics.map((data) => {
49+
index = index + 1;
50+
return {
51+
id: index,
52+
col1: new Date(Number(data.ts) / 1000000),
53+
col2: data.num_blocks_proposed,
54+
col3: data.num_blocks_failed,
55+
col4: data.failure_rate,
56+
col5: data.subnet_assigned,
57+
};
58+
});
59+
60+
const colDef: GridColDef[] = [
61+
{ field: 'col1', headerName: 'Date (UTC)', width: 200, valueFormatter: (value: Date) => formatDateToUTC(value)},
62+
{ field: 'col2', headerName: 'Blocks Proposed', width: 150 },
63+
{ field: 'col3', headerName: 'Blocks Failed', width: 150 },
64+
{ field: 'col4', headerName: 'Daily Failure Rate', width: 350 , valueFormatter: (value: number) => `${value * 100}%`,},
65+
{ field: 'col5', headerName: 'Subnet Assigned', width: 550 },
66+
];
67+
4668
return (
4769
<Box sx={{ p: 3 }}>
4870
<Paper sx={paperStyle}>
@@ -54,7 +76,8 @@ export const NodeChart: React.FC<NodeChartProps> = ({ nodeRewards, periodFilter
5476
<Divider/>
5577
</Grid>
5678
<Grid item xs={12} md={4}>
57-
<NodeInfo nodeId={nodeMetrics.node_id.toText()} nodeProviderId={nodeMetrics.node_provider_id.toText()} />
79+
<InfoFormatter name={"Node ID"} value={nodeMetrics.node_id.toText()} />
80+
<InfoFormatter name={"Node Provider ID"} value={nodeMetrics.node_provider_id.toText()} />
5881
</Grid>
5982
<Grid item xs={12} md={8}>
6083
<WidgetGauge value={rewardsPercent} title={"Rewards Total"} />
@@ -72,12 +95,12 @@ export const NodeChart: React.FC<NodeChartProps> = ({ nodeRewards, periodFilter
7295
<RewardsInfo failureRate={failureRateAvg} rewardReduction={rewardsReduction}/>
7396
</Grid>
7497
<Grid item xs={12} md={12}>
75-
<ExportCustomToolbar chartDailyData={chartDailyData}/>
98+
<ExportTable colDef={colDef} rows={rows}/>
7699
</Grid>
77100
</Grid>
78101
</Paper>
79102
</Box>
80103
);
81104
};
82105

83-
export default NodeChart;
106+
export default NodePage;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React, { useState } from 'react';
2+
import { Box, Grid, Paper, Typography } from '@mui/material';
3+
import { axisClasses, BarChart, StackOrderType } from '@mui/x-charts';
4+
import Divider from '@mui/material/Divider';
5+
import { useParams } from 'react-router-dom';
6+
import { formatDateToUTC, generateChartData, getFormattedDates } from '../utils/utils';
7+
import { PeriodFilter } from './FilterBar';
8+
import { Root } from './NodeList';
9+
import { NodeRewardsResponse } from '../../../declarations/trustworthy-node-metrics/trustworthy-node-metrics.did';
10+
import { paperStyle } from '../Styles';
11+
import InfoFormatter from './NodeInfo';
12+
import { ExportTable } from './ExportTable';
13+
import { GridColDef, GridRowsProp } from '@mui/x-data-grid';
14+
15+
export interface NodeProviderPageProps {
16+
nodeRewards: NodeRewardsResponse[],
17+
periodFilter: PeriodFilter
18+
}
19+
20+
export const NodeProviderPage: React.FC<NodeProviderPageProps> = ({ nodeRewards, periodFilter }) => {
21+
const { provider } = useParams();
22+
const providerNodeMetrics = nodeRewards
23+
.filter((nodeMetrics) => nodeMetrics.node_provider_id.toText() === provider)
24+
const highFailureRateChart = providerNodeMetrics
25+
.filter(nodeMetrics => nodeMetrics.rewards_stats.rewards_reduction > 0)
26+
.flatMap(nodeMetrics => {
27+
const chartData = generateChartData(periodFilter, nodeMetrics.daily_node_metrics);
28+
return {
29+
data: chartData.map(data => data.dailyNodeMetrics? data.dailyNodeMetrics.failure_rate * 100: null),
30+
label: nodeMetrics.node_id.toText(),
31+
stack: 'total'
32+
}
33+
});
34+
35+
let index = 0;
36+
const rows: GridRowsProp = providerNodeMetrics.flatMap((nodeRewards) => {
37+
return nodeRewards.daily_node_metrics.map((data) => {
38+
index = index + 1;
39+
return {
40+
id: index,
41+
col1: new Date(Number(data.ts) / 1000000),
42+
col2: nodeRewards.node_id,
43+
col3: data.num_blocks_proposed,
44+
col4: data.num_blocks_failed,
45+
col5: data.failure_rate,
46+
col6: data.subnet_assigned,
47+
};
48+
})
49+
});
50+
51+
const colDef: GridColDef[] = [
52+
{ field: 'col1', headerName: 'Date (UTC)', width: 200, valueFormatter: (value: Date) => formatDateToUTC(value)},
53+
{ field: 'col2', headerName: 'Node ID', width: 550 },
54+
{ field: 'col3', headerName: 'Blocks Proposed', width: 150 },
55+
{ field: 'col4', headerName: 'Blocks Failed', width: 150 },
56+
{ field: 'col5', headerName: 'Daily Failure Rate', width: 350 , valueFormatter: (value: number) => `${value * 100}%`,},
57+
{ field: 'col6', headerName: 'Subnet Assigned', width: 550 },
58+
];
59+
60+
return (
61+
62+
<Box sx={{ p: 3 }}>
63+
<Paper sx={paperStyle}>
64+
<Grid container spacing={3}>
65+
<Grid item xs={12} md={12}>
66+
<Typography gutterBottom variant="h5" component="div">
67+
{"Node Provider"}
68+
</Typography>
69+
<Divider/>
70+
</Grid>
71+
<Grid item xs={12} md={12}>
72+
<InfoFormatter name={"Provider ID"} value={provider ? provider : "Anonym"} />
73+
</Grid>
74+
<Grid item xs={12}>
75+
<Typography variant="h6" component="div">
76+
Daily Failure Rate
77+
</Typography>
78+
<Typography variant="subtitle2" sx={{ color: 'text.disabled' }} component="div">
79+
For nodes with rewards reduction
80+
</Typography>
81+
</Grid>
82+
<Grid item xs={12} md={12}>
83+
<BarChart
84+
slotProps={{ legend: { hidden: true } }}
85+
xAxis={[{
86+
scaleType: 'band',
87+
data: getFormattedDates(periodFilter),
88+
}]}
89+
yAxis={[{
90+
valueFormatter: (value: number) => `${value}%`,
91+
}]}
92+
leftAxis={null}
93+
borderRadius={9}
94+
series={highFailureRateChart}
95+
height={300}
96+
/>
97+
</Grid>
98+
<Grid item xs={12} md={12}>
99+
<ExportTable colDef={colDef} rows={rows}/>
100+
</Grid>
101+
</Grid>
102+
</Paper>
103+
</Box>
104+
);
105+
};

0 commit comments

Comments
 (0)