diff --git a/node/src/bin/manager.rs b/node/src/bin/manager.rs index ba1903cb004..902241d3d54 100644 --- a/node/src/bin/manager.rs +++ b/node/src/bin/manager.rs @@ -143,6 +143,12 @@ pub enum Command { /// List only used (current and pending) versions #[clap(long, short)] used: bool, + /// List names only for the active deployment + #[clap(long, short)] + brief: bool, + /// Do not print subgraph names + #[clap(long, short = 'N')] + no_name: bool, }, /// Manage unused deployments /// @@ -1127,6 +1133,8 @@ async fn main() -> anyhow::Result<()> { status, used, all, + brief, + no_name, } => { let (store, primary_pool) = ctx.store_and_primary(); @@ -1142,6 +1150,8 @@ async fn main() -> anyhow::Result<()> { status, used, all, + brief, + no_name, }; commands::deployment::info::run(ctx, args) diff --git a/node/src/manager/commands/deployment/info.rs b/node/src/manager/commands/deployment/info.rs index 1b4f646a212..417092d6e2d 100644 --- a/node/src/manager/commands/deployment/info.rs +++ b/node/src/manager/commands/deployment/info.rs @@ -1,4 +1,6 @@ +use std::collections::BTreeMap; use std::collections::HashMap; +use std::io; use std::sync::Arc; use anyhow::bail; @@ -12,7 +14,8 @@ use graphman::deployment::Deployment; use graphman::deployment::DeploymentSelector; use graphman::deployment::DeploymentVersionSelector; -use crate::manager::display::List; +use crate::manager::display::Columns; +use crate::manager::display::Row; pub struct Context { pub primary_pool: ConnectionPool, @@ -26,6 +29,8 @@ pub struct Args { pub status: bool, pub used: bool, pub all: bool, + pub brief: bool, + pub no_name: bool, } pub fn run(ctx: Context, args: Args) -> Result<()> { @@ -41,6 +46,8 @@ pub fn run(ctx: Context, args: Args) -> Result<()> { status, used, all, + brief, + no_name, } = args; let deployment = match deployment { @@ -65,8 +72,7 @@ pub fn run(ctx: Context, args: Args) -> Result<()> { None }; - print_info(deployments, statuses); - + render(brief, no_name, deployments, statuses); Ok(()) } @@ -85,77 +91,86 @@ fn make_deployment_version_selector( } } -fn print_info(deployments: Vec, statuses: Option>) { - let mut headers = vec![ - "Name", - "Status", - "Hash", - "Namespace", - "Shard", - "Active", - "Chain", - "Node ID", - ]; - - if statuses.is_some() { - headers.extend(vec![ - "Paused", - "Synced", - "Health", - "Earliest Block", - "Latest Block", - "Chain Head Block", - ]); - } +const NONE: &str = "---"; - let mut list = List::new(headers); +fn optional(s: Option) -> String { + s.map(|x| x.to_string()).unwrap_or(NONE.to_owned()) +} - const NONE: &str = "---"; +fn render( + brief: bool, + no_name: bool, + deployments: Vec, + statuses: Option>, +) { + fn name_and_status(deployment: &Deployment) -> String { + format!("{} ({})", deployment.name, deployment.version_status) + } - fn optional(s: Option) -> String { - s.map(|x| x.to_string()).unwrap_or(NONE.to_owned()) + fn number(n: Option) -> String { + n.map(|x| format!("{x}")).unwrap_or(NONE.to_owned()) } + let mut table = Columns::default(); + + let mut combined: BTreeMap<_, Vec<_>> = BTreeMap::new(); for deployment in deployments { - let mut row = vec![ - deployment.name, - deployment.version_status, - deployment.hash, - deployment.namespace, - deployment.shard, - deployment.is_active.to_string(), - deployment.chain, - optional(deployment.node_id), - ]; - - let status = statuses.as_ref().map(|x| x.get(&deployment.id)); - - match status { - Some(Some(status)) => { - row.extend(vec![ - optional(status.is_paused), - status.is_synced.to_string(), - status.health.as_str().to_string(), - status.earliest_block_number.to_string(), - optional(status.latest_block.as_ref().map(|x| x.number)), - optional(status.chain_head_block.as_ref().map(|x| x.number)), - ]); + let status = statuses.as_ref().and_then(|x| x.get(&deployment.id)); + combined + .entry(deployment.id) + .or_default() + .push((deployment, status)); + } + + let mut first = true; + for (_, deployments) in combined { + let deployment = &deployments[0].0; + if first { + first = false; + } else { + table.push_row(Row::separator()); + } + table.push_row([ + "Namespace", + &format!("{} [{}]", deployment.namespace, deployment.shard), + ]); + table.push_row(["Hash", &deployment.hash]); + if !no_name && (!brief || deployment.is_active) { + if deployments.len() > 1 { + table.push_row(["Versions", &name_and_status(deployment)]); + for (d, _) in &deployments[1..] { + table.push_row(["", &name_and_status(d)]); + } + } else { + table.push_row(["Version", &name_and_status(deployment)]); } - Some(None) => { - row.extend(vec![ - NONE.to_owned(), - NONE.to_owned(), - NONE.to_owned(), - NONE.to_owned(), - NONE.to_owned(), - NONE.to_owned(), - ]); + table.push_row(["Chain", &deployment.chain]); + } + table.push_row(["Node ID", &optional(deployment.node_id.as_ref())]); + table.push_row(["Active", &deployment.is_active.to_string()]); + if let Some((_, status)) = deployments.get(0) { + if let Some(status) = status { + table.push_row(["Paused", &optional(status.is_paused)]); + table.push_row(["Synced", &status.is_synced.to_string()]); + table.push_row(["Health", status.health.as_str()]); + + let earliest = status.earliest_block_number; + let latest = status.latest_block.as_ref().map(|x| x.number); + let chain_head = status.chain_head_block.as_ref().map(|x| x.number); + let behind = match (latest, chain_head) { + (Some(latest), Some(chain_head)) => Some(chain_head - latest), + _ => None, + }; + + table.push_row(["Earliest Block", &earliest.to_string()]); + table.push_row(["Latest Block", &number(latest)]); + table.push_row(["Chain Head Block", &number(chain_head)]); + if let Some(behind) = behind { + table.push_row([" Blocks behind", &behind.to_string()]); + } } - None => {} } - - list.append(row); } - list.render(); + table.render(&mut io::stdout()).ok(); } diff --git a/node/src/manager/display.rs b/node/src/manager/display.rs index 694eaf629bf..7d27b8269cb 100644 --- a/node/src/manager/display.rs +++ b/node/src/manager/display.rs @@ -1,3 +1,7 @@ +use std::io::{self, Write}; + +const LINE_WIDTH: usize = 78; + pub struct List { pub headers: Vec, pub rows: Vec>, @@ -29,8 +33,6 @@ impl List { } pub fn render(&self) { - const LINE_WIDTH: usize = 78; - let header_width = self.headers.iter().map(|h| h.len()).max().unwrap_or(0); let header_width = if header_width < 5 { 5 } else { header_width }; let mut first = true; @@ -52,3 +54,97 @@ impl List { } } } + +/// A more general list of columns than `List`. In practical terms, this is +/// a very simple table with two columns, where both columns are +/// left-aligned +pub struct Columns { + widths: Vec, + rows: Vec, +} + +impl Columns { + pub fn push_row>(&mut self, row: R) { + let row = row.into(); + for (idx, width) in row.widths().iter().enumerate() { + if idx >= self.widths.len() { + self.widths.push(*width); + } else { + self.widths[idx] = (*width).max(self.widths[idx]); + } + } + self.rows.push(row); + } + + pub fn render(&self, out: &mut dyn Write) -> io::Result<()> { + for row in &self.rows { + row.render(out, &self.widths)?; + } + Ok(()) + } +} + +impl Default for Columns { + fn default() -> Self { + Self { + widths: Vec::new(), + rows: Vec::new(), + } + } +} + +pub enum Row { + Cells(Vec), + Separator, +} + +impl Row { + pub fn separator() -> Self { + Self::Separator + } + + fn widths(&self) -> Vec { + match self { + Row::Cells(cells) => cells.iter().map(|cell| cell.len()).collect(), + Row::Separator => vec![], + } + } + + fn render(&self, out: &mut dyn Write, widths: &[usize]) -> io::Result<()> { + match self { + Row::Cells(cells) => { + for (idx, cell) in cells.iter().enumerate() { + if idx > 0 { + write!(out, " | ")?; + } + write!(out, "{cell:width$}", width = widths[idx])?; + } + } + Row::Separator => { + let total_width = widths.iter().sum::(); + let extra_width = if total_width >= LINE_WIDTH { + 0 + } else { + LINE_WIDTH - total_width + }; + for (idx, width) in widths.iter().enumerate() { + if idx > 0 { + write!(out, "-+-")?; + } + if idx == widths.len() - 1 { + write!(out, "{:- for Row { + fn from(row: [&str; 2]) -> Self { + Self::Cells(row.iter().map(|s| s.to_string()).collect()) + } +}