1use crate::text_value::REPORT_TABLE_OPTIONS;
8use ic_host_text::{
9 escape::terminal_text,
10 table::{Alignment, render},
11};
12
13#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
14use super::CloudEngineListReport;
15use super::{CloudEngineOperatorReport, CloudEnginePricesReport, CloudEngineReportContext};
16use crate::{
17 human_quantity::decimal_cycle_count_text,
18 text_value::{optional_text, yes_no},
19};
20
21#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
23#[must_use]
24pub fn cloud_engine_list_report_text(report: &CloudEngineListReport) -> String {
25 let mut lines = list_context_lines(report);
26 lines.push(String::new());
27 lines.push("CloudEngine subnets".to_string());
28 if report.cloud_engines.is_empty() {
29 lines.push("none".to_string());
30 return lines.join("\n");
31 }
32 lines.push(list_table(report));
33 append_lookup_failures(report, &mut lines);
34 lines.join("\n")
35}
36
37#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
38fn list_context_lines(report: &CloudEngineListReport) -> Vec<String> {
39 vec![
40 format!("network: {}", terminal_text(&report.network)),
41 format!(
42 "registry_authority: {}",
43 terminal_text(&report.registry_authority)
44 ),
45 format!("registry_canister_id: {}", report.registry_canister_id),
46 format!("registry_version: {}", report.registry_version),
47 format!("registry_assurance: {}", report.registry_assurance.as_str()),
48 format!(
49 "registry_source_endpoints: {}",
50 report
51 .registry_source_endpoints
52 .iter()
53 .map(|endpoint| terminal_text(endpoint))
54 .collect::<Vec<_>>()
55 .join(", ")
56 ),
57 format!(
58 "catalog_fetched_at: {}",
59 terminal_text(&report.catalog_fetched_at)
60 ),
61 format!(
62 "catalog_cache_disposition: {}",
63 report.catalog_cache_disposition.as_str()
64 ),
65 format!("catalog_stale: {}", yes_no(report.catalog_stale)),
66 String::new(),
67 format!(
68 "control_plane_authority: {}",
69 terminal_text(&report.control_plane_authority)
70 ),
71 format!(
72 "control_plane_canister_id: {}",
73 report.control_plane_canister_id
74 ),
75 format!(
76 "control_plane_source_endpoint: {}",
77 terminal_text(&report.control_plane_source_endpoint)
78 ),
79 format!(
80 "control_plane_fetched_at: {}",
81 terminal_text(&report.control_plane_fetched_at)
82 ),
83 format!(
84 "control_plane_certified: {}",
85 yes_no(report.control_plane_certified)
86 ),
87 format!(
88 "control_plane_point_in_time_guaranteed: {}",
89 yes_no(report.control_plane_point_in_time_guaranteed)
90 ),
91 format!(
92 "control_plane_lookup_attempt_count: {}",
93 report.control_plane_lookup_attempt_count
94 ),
95 format!(
96 "operator_bindings: {} resolved, {} absent, {} failed",
97 report.operator_binding_count,
98 report.missing_operator_binding_count,
99 report.operator_lookup_failure_count
100 ),
101 ]
102}
103
104#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
105fn list_table(report: &CloudEngineListReport) -> String {
106 let headers = ["Label", "Subnet", "Nodes", "Binding", "Operator"];
107 let alignments = [
108 Alignment::Left,
109 Alignment::Left,
110 Alignment::Right,
111 Alignment::Left,
112 Alignment::Left,
113 ];
114 let rows = report
115 .cloud_engines
116 .iter()
117 .map(|row| {
118 [
119 terminal_text(&row.subnet_label).into_owned(),
120 row.subnet_id.clone(),
121 row.node_count
122 .map_or_else(|| "unknown".to_string(), |count| count.to_string()),
123 match &row.operator {
124 Ok(Some(_)) => "resolved",
125 Ok(None) => "absent",
126 Err(_) => "failed",
127 }
128 .to_string(),
129 row.operator
130 .as_ref()
131 .ok()
132 .and_then(Option::as_ref)
133 .map_or_else(
134 || "-".to_string(),
135 ic_platform::candid::contract::Principal::to_text,
136 ),
137 ]
138 })
139 .collect::<Vec<_>>();
140 render(&headers, &rows, &alignments, REPORT_TABLE_OPTIONS)
141}
142
143#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
144fn append_lookup_failures(report: &CloudEngineListReport, lines: &mut Vec<String>) {
145 let failures = report
146 .cloud_engines
147 .iter()
148 .filter_map(|row| row.operator.as_ref().err().map(|error| (row, error)))
149 .collect::<Vec<_>>();
150 if !failures.is_empty() {
151 lines.push(String::new());
152 lines.push("Operator lookup failures".to_string());
153 lines.extend(
154 failures
155 .into_iter()
156 .map(|(row, error)| format!(" {}: {}", row.subnet_id, terminal_text(error))),
157 );
158 }
159}
160
161#[must_use]
163pub fn cloud_engine_operator_report_text(report: &CloudEngineOperatorReport) -> String {
164 let mut lines = context_lines(&report.context);
165 lines.extend([
166 format!("subnet_id: {}", report.subnet_id),
167 format!(
168 "operator_binding_present: {}",
169 yes_no(report.operator_binding_present)
170 ),
171 format!(
172 "operator_canister_id: {}",
173 optional_text(report.operator_canister_id.as_ref())
174 ),
175 format!(
176 "engine_owner: {}",
177 optional_text(report.engine_owner.as_ref())
178 ),
179 format!(
180 "platform_admin: {}",
181 optional_text(report.platform_admin.as_ref())
182 ),
183 format!(
184 "caffeine_enabled: {}",
185 report
186 .caffeine_enabled
187 .map_or_else(|| "-".to_string(), |enabled| yes_no(enabled).to_string())
188 ),
189 format!(
190 "claimed_domain_count: {}",
191 report
192 .claimed_domain_count
193 .map_or_else(|| "-".to_string(), |count| count.to_string())
194 ),
195 ]);
196
197 if let Some(domains) = report.claimed_domains.as_ref()
198 && !domains.is_empty()
199 {
200 lines.push(String::new());
201 lines.push("Claimed domains".to_string());
202 lines.extend(
203 domains
204 .iter()
205 .map(|domain| format!(" {}", terminal_text(domain))),
206 );
207 }
208 lines.join("\n")
209}
210
211#[must_use]
213pub fn cloud_engine_prices_report_text(report: &CloudEnginePricesReport) -> String {
214 let mut lines = context_lines(&report.context);
215 lines.extend([
216 format!("network_fee: {}", report.network_fee),
217 format!("price_count: {}", report.price_count),
218 ]);
219
220 if !report.prices.is_empty() {
221 let headers = [
222 "Key",
223 "Node type",
224 "Data center",
225 "Provider",
226 "Net/month",
227 "Gross/month",
228 "Updated (ns)",
229 ];
230 let alignments = [
231 Alignment::Left,
232 Alignment::Left,
233 Alignment::Left,
234 Alignment::Left,
235 Alignment::Right,
236 Alignment::Right,
237 Alignment::Right,
238 ];
239 let rows = report
240 .prices
241 .iter()
242 .map(|row| {
243 [
244 row.key.clone(),
245 row.node_type.to_string(),
246 row.data_center_id.as_deref().map_or_else(
247 || "-".to_string(),
248 |value| terminal_text(value).into_owned(),
249 ),
250 row.provider_id.clone().unwrap_or_else(|| "-".to_string()),
251 decimal_cycle_count_text(&row.net_cycles_per_month),
252 decimal_cycle_count_text(&row.gross_cycles_per_month),
253 row.updated_at_unix_nanos.to_string(),
254 ]
255 })
256 .collect::<Vec<_>>();
257 lines.push(String::new());
258 lines.push("Marketplace prices".to_string());
259 lines.push(render(&headers, &rows, &alignments, REPORT_TABLE_OPTIONS));
260 }
261 lines.join("\n")
262}
263
264fn context_lines(context: &CloudEngineReportContext) -> Vec<String> {
265 vec![
266 format!("network: {}", terminal_text(&context.network)),
267 format!("authority: {}", terminal_text(&context.authority)),
268 format!("engine_canister_id: {}", context.engine_canister_id),
269 format!("fetched_at: {}", terminal_text(&context.fetched_at)),
270 format!(
271 "source_endpoint: {}",
272 terminal_text(&context.source_endpoint)
273 ),
274 format!("fetched_by: {}", terminal_text(&context.fetched_by)),
275 format!("certified: {}", yes_no(context.certified)),
276 format!(
277 "point_in_time_guaranteed: {}",
278 yes_no(context.point_in_time_guaranteed)
279 ),
280 format!("query_call_count: {}", context.query_call_count),
281 ]
282}