Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(cketh): add pagination to minter dashboard #3046

Merged
merged 7 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rs/ethereum/cketh/minter/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ rust_test(
srcs = glob(["src/**/*.rs"]),
compile_data = [
"templates/dashboard.html",
"templates/pagination.html",
"templates/principal_to_bytes.js",
],
crate_features = features,
Expand Down
160 changes: 153 additions & 7 deletions rs/ethereum/cketh/minter/src/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod tests;

use askama::Template;
use candid::{Nat, Principal};
use ic_canisters_http_types::HttpRequest;
use ic_cketh_minter::endpoints::{EthTransaction, RetrieveEthStatus};
use ic_cketh_minter::erc20::CkTokenSymbol;
use ic_cketh_minter::eth_logs::{EventSource, ReceivedEvent};
Expand All @@ -22,6 +23,7 @@ use ic_ethereum_types::Address;
use icrc_ledger_types::icrc1::account::Account;
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet};
use std::str::FromStr;

mod filters {
pub fn timestamp_to_datetime<T: std::fmt::Display>(timestamp: T) -> askama::Result<String> {
Expand Down Expand Up @@ -145,6 +147,123 @@ impl DashboardPendingDeposit {
}
}

// Number of entries per page in dashboard tables (e.g. minted events, finalized transactions).
const DEFAULT_PAGE_SIZE: usize = 100;

#[derive(Default, Clone)]
pub struct DashboardPaginationParameters {
minted_events_start: usize,
finalized_transactions_start: usize,
reimbursed_transactions_start: usize,
}

impl DashboardPaginationParameters {
pub(crate) fn from_query_params(
req: &HttpRequest,
) -> Result<DashboardPaginationParameters, String> {
fn parse_query_param(req: &HttpRequest, param_name: &str) -> Result<usize, String> {
Ok(match req.raw_query_param(param_name) {
Some(arg) => usize::from_str(arg)
.map_err(|_| format!("failed to parse the '{}' parameter", param_name))?,
None => 0,
})
}

Ok(Self {
minted_events_start: parse_query_param(req, "minted_events_start")?,
finalized_transactions_start: parse_query_param(req, "finalized_transactions_start")?,
reimbursed_transactions_start: parse_query_param(req, "reimbursed_transactions_start")?,
})
}
}

#[derive(Clone)]
pub struct DashboardPaginatedTable<T> {
current_page: Vec<T>,
pagination: DashboardTablePagination,
}

impl<T: Clone> DashboardPaginatedTable<T> {
pub fn from_items(
items: &[T],
current_page_offset: usize,
page_size: usize,
num_cols: usize,
table_reference: &str,
page_offset_query_param: &str,
) -> Self {
Self {
current_page: items
.iter()
.skip(current_page_offset)
.take(page_size)
.cloned()
.collect(),
pagination: DashboardTablePagination::pagination(
items.len(),
current_page_offset,
page_size,
num_cols,
table_reference,
page_offset_query_param,
),
}
}

pub fn is_empty(&self) -> bool {
self.current_page.is_empty()
}

pub fn has_more_than_one_page(&self) -> bool {
self.pagination.pages.len() > 1
}
}

#[derive(Clone)]
pub struct DashboardTablePage {
index: usize,
offset: usize,
}

#[derive(Template)]
#[template(path = "pagination.html")]
#[derive(Clone)]
pub struct DashboardTablePagination {
pub table_id: String,
pub table_width: usize,
pub page_offset_query_param: String,
pub current_page_index: usize,
pub pages: Vec<DashboardTablePage>,
}

impl DashboardTablePagination {
fn pagination(
num_items: usize,
current_offset: usize,
page_size: usize,
table_width: usize,
table_reference: &str,
page_offset_query_param: &str,
) -> Self {
let pages = (0..num_items)
.step_by(page_size)
.enumerate()
.map(|(index, offset)| DashboardTablePage {
index: index + 1,
offset,
})
.collect();
let current_page_index = current_offset / page_size + 1;
Self {
table_id: String::from(table_reference),
page_offset_query_param: String::from(page_offset_query_param),
table_width,
current_page_index,
pages,
}
}
}

#[derive(Template)]
#[template(path = "dashboard.html")]
#[derive(Clone)]
Expand All @@ -158,26 +277,35 @@ pub struct DashboardTemplate {
pub first_synced_block: BlockNumber,
pub last_observed_block: Option<BlockNumber>,
pub cketh_ledger_id: Principal,
pub minted_events: Vec<MintedEvent>,
pub minted_events_table: DashboardPaginatedTable<MintedEvent>,
pub pending_deposits: Vec<DashboardPendingDeposit>,
pub invalid_events: BTreeMap<EventSource, InvalidEventReason>,
pub withdrawal_requests: Vec<DashboardWithdrawalRequest>,
pub pending_transactions: Vec<DashboardPendingTransaction>,
pub finalized_transactions: Vec<DashboardFinalizedTransaction>,
pub reimbursed_transactions: Vec<DashboardReimbursedTransaction>,
pub finalized_transactions_table: DashboardPaginatedTable<DashboardFinalizedTransaction>,
pub reimbursed_transactions_table: DashboardPaginatedTable<DashboardReimbursedTransaction>,
pub eth_balance: EthBalance,
pub skipped_blocks: BTreeMap<String, BTreeSet<BlockNumber>>,
pub supported_ckerc20_tokens: Vec<DashboardCkErc20Token>,
}

impl DashboardTemplate {
pub fn from_state(state: &State) -> Self {
pub fn from_state(state: &State, pagination_parameters: DashboardPaginationParameters) -> Self {
let mut minted_events: Vec<_> = state.minted_events.values().cloned().collect();
minted_events.sort_unstable_by_key(|event| {
let deposit_event = &event.deposit_event;
Reverse((deposit_event.block_number(), deposit_event.log_index()))
});

let minted_events_table = DashboardPaginatedTable::from_items(
&minted_events,
pagination_parameters.minted_events_start,
DEFAULT_PAGE_SIZE,
7,
"minted-events",
"minted_events_start",
);

let mut supported_ckerc20_tokens: Vec<_> = state
.supported_ck_erc20_tokens()
.map(|ckerc20| DashboardCkErc20Token {
Expand Down Expand Up @@ -279,6 +407,15 @@ impl DashboardTemplate {
.collect();
finalized_transactions.sort_unstable_by_key(|tx| Reverse(tx.ledger_burn_index));

let finalized_transactions_table = DashboardPaginatedTable::from_items(
&finalized_transactions,
pagination_parameters.finalized_transactions_start,
DEFAULT_PAGE_SIZE,
8,
"finalized-transactions",
"finalized_transactions_start",
);

let mut reimbursed_transactions: Vec<_> = state
.eth_transactions
.reimbursed_transactions_iter()
Expand Down Expand Up @@ -321,6 +458,15 @@ impl DashboardTemplate {
reimbursed_transactions
.sort_unstable_by_key(|reimbursed_tx| Reverse(reimbursed_tx.cketh_ledger_burn_index()));

let reimbursed_transactions_table = DashboardPaginatedTable::from_items(
&reimbursed_transactions,
pagination_parameters.reimbursed_transactions_start,
DEFAULT_PAGE_SIZE,
6,
"reimbursed-transactions",
"reimbursed_transactions_start",
);

DashboardTemplate {
ethereum_network: state.ethereum_network,
ecdsa_key_name: state.ecdsa_key_name.clone(),
Expand All @@ -334,13 +480,13 @@ impl DashboardTemplate {
minimum_withdrawal_amount: state.cketh_minimum_withdrawal_amount,
first_synced_block: state.first_scraped_block_number,
last_observed_block: state.last_observed_block_number,
minted_events,
minted_events_table,
pending_deposits,
invalid_events: state.invalid_events.clone(),
withdrawal_requests,
pending_transactions,
finalized_transactions,
reimbursed_transactions,
finalized_transactions_table,
reimbursed_transactions_table,
eth_balance: state.eth_balance.clone(),
skipped_blocks: state
.skipped_blocks
Expand Down
Loading
Loading