Skip to content
Merged
15 changes: 14 additions & 1 deletion src/commands/billing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,26 @@ impl Render for PaymentsView {
}
};
let mut t = new_table(ctx);
set_header_bold(&mut t, ctx, vec!["CREATED", "AMOUNT", "CURRENCY", "CARD"]);
set_header_bold(
&mut t,
ctx,
vec![
"CREATED",
"AMOUNT",
"CURRENCY",
"STATUS",
"CARD",
"MARKETPLACE",
],
);
for p in &data.payments {
t.add_row(vec![
Cell::new(&p.created_at),
Cell::new(&p.amount),
Cell::new(&p.currency),
Cell::new(&p.status),
opt_cell(&p.card_last_4),
opt_cell(&p.marketplace_amount),
]);
}
write_table(w, &t)
Expand Down
90 changes: 87 additions & 3 deletions src/commands/endpoint/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ impl Render for SingleEndpointView {
]);
t.add_row(vec![Cell::new("http_url"), Cell::new(&e.http_url)]);
t.add_row(vec![Cell::new("wss_url"), opt_cell(&e.wss_url)]);
if !e.tags.is_empty() {
if e.tags.is_empty() {
t.add_row(vec![Cell::new("tags"), Cell::new("—")]);
} else {
let tags = e
.tags
.iter()
Expand All @@ -111,7 +113,88 @@ impl Render for SingleEndpointView {
.join(", ");
t.add_row(vec![Cell::new("tags"), Cell::new(tags)]);
}
write_table(w, &t)
t.add_row(vec![
Cell::new("is_multichain"),
bool_cell(Some(e.is_multichain)),
]);
if let Some(sec) = &e.security {
let opts = sec.options.as_ref();
let feature = |enabled: Option<bool>, count: usize| {
let mark = match enabled {
Some(true) => "✓",
Some(false) => "✗",
None => "—",
};
Cell::new(format!("{mark} ({count})"))
};
t.add_row(vec![
Cell::new("security.tokens"),
feature(
opts.and_then(|o| o.tokens),
sec.tokens.as_deref().unwrap_or(&[]).len(),
),
]);
t.add_row(vec![
Cell::new("security.jwts"),
feature(
opts.and_then(|o| o.jwts),
sec.jwts.as_deref().unwrap_or(&[]).len(),
),
]);
t.add_row(vec![
Cell::new("security.domain_masks"),
feature(
opts.and_then(|o| o.domain_masks),
sec.domain_masks.as_deref().unwrap_or(&[]).len(),
),
]);
t.add_row(vec![
Cell::new("security.ips"),
feature(
opts.and_then(|o| o.ips),
sec.ips.as_deref().unwrap_or(&[]).len(),
),
]);
t.add_row(vec![
Cell::new("security.referrers"),
feature(
opts.and_then(|o| o.referrers),
sec.referrers.as_deref().unwrap_or(&[]).len(),
),
]);
t.add_row(vec![
Cell::new("security.request_filters"),
feature(
opts.and_then(|o| o.request_filters),
sec.request_filters.as_deref().unwrap_or(&[]).len(),
),
]);
let ip_header = opts
.and_then(|o| o.ip_custom_header.as_ref())
.and_then(|h| h.value.clone());
t.add_row(vec![
Cell::new("security.ip_custom_header"),
opt_cell(&ip_header),
]);
}
if let Some(rl) = &e.rate_limits {
t.add_row(vec![
Cell::new("rate_limits.by_ip"),
bool_cell(rl.rate_limit_by_ip),
]);
t.add_row(vec![
Cell::new("rate_limits.account"),
opt_cell(&rl.account),
]);
t.add_row(vec![Cell::new("rate_limits.rps"), opt_cell(&rl.rps)]);
t.add_row(vec![Cell::new("rate_limits.rpm"), opt_cell(&rl.rpm)]);
t.add_row(vec![Cell::new("rate_limits.rpd"), opt_cell(&rl.rpd)]);
}
write_table(w, &t)?;
if let Some(sec) = &e.security {
super::security::security_item_sections(w, ctx, sec)?;
}
Ok(())
}
}

Expand Down Expand Up @@ -160,13 +243,14 @@ impl Render for EndpointLogsView {
set_header_bold(
&mut t,
ctx,
vec!["TIME", "METHOD", "STATUS", "NETWORK", "REQUEST_ID"],
vec!["TIME", "METHOD", "STATUS", "ERROR", "NETWORK", "REQUEST_ID"],
);
for l in &self.0.data {
t.add_row(vec![
Cell::new(&l.timestamp),
opt_cell(&l.method),
opt_cell(&l.status),
opt_cell(&l.error_code),
opt_cell(&l.network),
opt_cell(&l.request_id),
]);
Expand Down
155 changes: 103 additions & 52 deletions src/commands/endpoint/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use serde::Serialize;
use crate::confirm::confirm_mild;
use crate::context::Ctx;
use crate::errors::CliError;
use crate::output::{new_table, opt_cell, set_header_bold, write_table, Render};
use crate::output::{bool_cell, new_table, opt_cell, set_header_bold, write_table, Render};
use crate::retry::retrying;

#[derive(Debug, Subcommand)]
Expand Down Expand Up @@ -458,75 +458,126 @@ impl Render for SecurityShowView {
return Ok(());
}
};
let opts = data.options.as_ref();
let mut t = new_table(ctx);
set_header_bold(&mut t, ctx, vec!["FEATURE", "COUNT", "DETAIL"]);
let tokens = data.tokens.as_deref().unwrap_or(&[]);
set_header_bold(&mut t, ctx, vec!["OPTION", "ENABLED"]);
t.add_row(vec![
Cell::new("tokens"),
Cell::new(tokens.len()),
Cell::new(""),
bool_cell(opts.and_then(|o| o.tokens)),
]);
let referrers = data.referrers.as_deref().unwrap_or(&[]);
t.add_row(vec![
Cell::new("referrers"),
Cell::new(referrers.len()),
Cell::new(
referrers
.iter()
.filter_map(|r| r.referrer.clone())
.collect::<Vec<_>>()
.join(", "),
),
]);
let ips = data.ips.as_deref().unwrap_or(&[]);
t.add_row(vec![
Cell::new("ips"),
Cell::new(ips.len()),
Cell::new(
ips.iter()
.map(|i| i.ip.clone())
.collect::<Vec<_>>()
.join(", "),
),
]);
let jwts = data.jwts.as_deref().unwrap_or(&[]);
t.add_row(vec![
Cell::new("jwts"),
Cell::new(jwts.len()),
Cell::new(""),
bool_cell(opts.and_then(|o| o.jwts)),
]);
let masks = data.domain_masks.as_deref().unwrap_or(&[]);
t.add_row(vec![
Cell::new("domain_masks"),
Cell::new(masks.len()),
Cell::new(
masks
.iter()
.map(|d| d.domain.clone())
.collect::<Vec<_>>()
.join(", "),
),
bool_cell(opts.and_then(|o| o.domain_masks)),
]);
t.add_row(vec![Cell::new("ips"), bool_cell(opts.and_then(|o| o.ips))]);
t.add_row(vec![
Cell::new("referrers"),
bool_cell(opts.and_then(|o| o.referrers)),
]);
let filters = data.request_filters.as_deref().unwrap_or(&[]);
t.add_row(vec![
Cell::new("request_filters"),
Cell::new(filters.len()),
Cell::new(""),
bool_cell(opts.and_then(|o| o.request_filters)),
]);
let ip_header = data
.options
.as_ref()
let ip_header = opts
.and_then(|o| o.ip_custom_header.as_ref())
.and_then(|h| h.value.clone());
t.add_row(vec![
Cell::new("ip_custom_header"),
opt_cell(&ip_header),
Cell::new(""),
]);
write_table(w, &t)
t.add_row(vec![Cell::new("ip_custom_header"), opt_cell(&ip_header)]);
write_table(w, &t)?;
security_item_sections(w, ctx, data)
}
}

/// Renders one titled table per configured security item list (TOKENS, JWTS,
/// ...). Lists with no items render no section so lightly-configured
/// endpoints stay compact. Shared between `endpoint security show` and
/// `endpoint show`.
pub(crate) fn security_item_sections(
w: &mut dyn std::io::Write,
ctx: &crate::output::OutputCtx,
data: &quicknode_sdk::admin::EndpointSecurity,
) -> std::io::Result<()> {
let section = |w: &mut dyn std::io::Write,
title: &str,
headers: Vec<&str>,
rows: Vec<Vec<Cell>>|
-> std::io::Result<()> {
if rows.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, "{} ({})", title, rows.len())?;
let mut t = new_table(ctx);
set_header_bold(&mut t, ctx, headers);
for row in rows {
t.add_row(row);
}
write_table(w, &t)
};

let tokens = data.tokens.as_deref().unwrap_or(&[]);
section(
w,
"TOKENS",
vec!["ID", "TOKEN"],
tokens
.iter()
.map(|t| vec![Cell::new(&t.id), Cell::new(&t.token)])
.collect(),
)?;
let jwts = data.jwts.as_deref().unwrap_or(&[]);
section(
w,
"JWTS",
vec!["ID", "NAME", "KID"],
jwts.iter()
.map(|j| vec![Cell::new(&j.id), Cell::new(&j.name), Cell::new(&j.kid)])
.collect(),
)?;
let referrers = data.referrers.as_deref().unwrap_or(&[]);
section(
w,
"REFERRERS",
vec!["ID", "REFERRER"],
referrers
.iter()
.map(|r| vec![Cell::new(&r.id), opt_cell(&r.referrer)])
.collect(),
)?;
let masks = data.domain_masks.as_deref().unwrap_or(&[]);
section(
w,
"DOMAIN_MASKS",
vec!["ID", "DOMAIN"],
masks
.iter()
.map(|d| vec![Cell::new(&d.id), Cell::new(&d.domain)])
.collect(),
)?;
let ips = data.ips.as_deref().unwrap_or(&[]);
section(
w,
"IPS",
vec!["ID", "IP"],
ips.iter()
.map(|i| vec![Cell::new(&i.id), Cell::new(&i.ip)])
.collect(),
)?;
let filters = data.request_filters.as_deref().unwrap_or(&[]);
section(
w,
"REQUEST_FILTERS",
vec!["ID", "METHODS"],
filters
.iter()
.map(|f| vec![Cell::new(&f.id), Cell::new(f.method.join(", "))])
.collect(),
)
}

#[derive(Serialize)]
struct SecurityOptionsView(quicknode_sdk::admin::GetSecurityOptionsResponse);

Expand Down
31 changes: 30 additions & 1 deletion src/commands/team.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,36 @@ impl Render for TeamDetailView {
Cell::new("default_role"),
opt_cell(&detail.default_role),
]);
write_table(w, &t)
t.add_row(vec![
Cell::new("members_count"),
opt_cell(&detail.members_count),
]);
write_table(w, &t)?;

let member_section = |w: &mut dyn std::io::Write,
title: &str,
users: &[quicknode_sdk::admin::TeamUser]|
-> std::io::Result<()> {
if users.is_empty() {
return Ok(());
}
writeln!(w)?;
writeln!(w, "{} ({})", title, users.len())?;
let mut t = new_table(ctx);
set_header_bold(&mut t, ctx, vec!["ID", "EMAIL", "NAME", "ROLE", "STATUS"]);
for u in users {
t.add_row(vec![
Cell::new(u.id),
Cell::new(&u.email),
opt_cell(&u.full_name),
opt_cell(&u.role),
opt_cell(&u.status),
]);
}
write_table(w, &t)
};
member_section(w, "MEMBERS", &detail.users)?;
member_section(w, "PENDING_INVITES", &detail.pending_invites)
}
}

Expand Down
Loading
Loading