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, CloudEngineOperatorLookupStatus};
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),
120 row.subnet_id.clone(),
121 row.node_count
122 .map_or_else(|| "unknown".to_string(), |count| count.to_string()),
123 row.operator_lookup_status.as_str().to_string(),
124 row.operator_canister_id
125 .clone()
126 .unwrap_or_else(|| "-".to_string()),
127 ]
128 })
129 .collect::<Vec<_>>();
130 render(&headers, &rows, &alignments, REPORT_TABLE_OPTIONS)
131}
132
133#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
134fn append_lookup_failures(report: &CloudEngineListReport, lines: &mut Vec<String>) {
135 let failures = report
136 .cloud_engines
137 .iter()
138 .filter(|row| row.operator_lookup_status == CloudEngineOperatorLookupStatus::Failed)
139 .collect::<Vec<_>>();
140 if !failures.is_empty() {
141 lines.push(String::new());
142 lines.push("Operator lookup failures".to_string());
143 lines.extend(failures.into_iter().map(|row| {
144 format!(
145 " {}: {}",
146 row.subnet_id,
147 terminal_text(
148 row.operator_lookup_error
149 .as_deref()
150 .unwrap_or("unspecified lookup failure")
151 )
152 )
153 }));
154 }
155}
156
157#[must_use]
159pub fn cloud_engine_operator_report_text(report: &CloudEngineOperatorReport) -> String {
160 let mut lines = context_lines(&report.context);
161 lines.extend([
162 format!("subnet_id: {}", report.subnet_id),
163 format!(
164 "operator_binding_present: {}",
165 yes_no(report.operator_binding_present)
166 ),
167 format!(
168 "operator_canister_id: {}",
169 optional_text(report.operator_canister_id.as_ref())
170 ),
171 format!(
172 "engine_owner: {}",
173 optional_text(report.engine_owner.as_ref())
174 ),
175 format!(
176 "platform_admin: {}",
177 optional_text(report.platform_admin.as_ref())
178 ),
179 format!(
180 "caffeine_enabled: {}",
181 report
182 .caffeine_enabled
183 .map_or_else(|| "-".to_string(), |enabled| yes_no(enabled).to_string())
184 ),
185 format!(
186 "claimed_domain_count: {}",
187 report
188 .claimed_domain_count
189 .map_or_else(|| "-".to_string(), |count| count.to_string())
190 ),
191 ]);
192
193 if let Some(domains) = report.claimed_domains.as_ref()
194 && !domains.is_empty()
195 {
196 lines.push(String::new());
197 lines.push("Claimed domains".to_string());
198 lines.extend(
199 domains
200 .iter()
201 .map(|domain| format!(" {}", terminal_text(domain))),
202 );
203 }
204 lines.join("\n")
205}
206
207#[must_use]
209pub fn cloud_engine_prices_report_text(report: &CloudEnginePricesReport) -> String {
210 let mut lines = context_lines(&report.context);
211 lines.extend([
212 format!("network_fee: {}", report.network_fee),
213 format!("price_count: {}", report.price_count),
214 ]);
215
216 if !report.prices.is_empty() {
217 let headers = [
218 "Key",
219 "Node type",
220 "Data center",
221 "Provider",
222 "Net/month",
223 "Gross/month",
224 "Updated (ns)",
225 ];
226 let alignments = [
227 Alignment::Left,
228 Alignment::Left,
229 Alignment::Left,
230 Alignment::Left,
231 Alignment::Right,
232 Alignment::Right,
233 Alignment::Right,
234 ];
235 let rows = report
236 .prices
237 .iter()
238 .map(|row| {
239 [
240 row.key.clone(),
241 row.node_type.to_string(),
242 row.data_center_id
243 .as_deref()
244 .map_or_else(|| "-".to_string(), terminal_text),
245 row.provider_id.clone().unwrap_or_else(|| "-".to_string()),
246 decimal_cycle_count_text(&row.net_cycles_per_month),
247 decimal_cycle_count_text(&row.gross_cycles_per_month),
248 row.updated_at_unix_nanos.to_string(),
249 ]
250 })
251 .collect::<Vec<_>>();
252 lines.push(String::new());
253 lines.push("Marketplace prices".to_string());
254 lines.push(render(&headers, &rows, &alignments, REPORT_TABLE_OPTIONS));
255 }
256 lines.join("\n")
257}
258
259fn context_lines(context: &CloudEngineReportContext) -> Vec<String> {
260 vec![
261 format!("network: {}", terminal_text(&context.network)),
262 format!("authority: {}", terminal_text(&context.authority)),
263 format!("engine_canister_id: {}", context.engine_canister_id),
264 format!("fetched_at: {}", terminal_text(&context.fetched_at)),
265 format!(
266 "source_endpoint: {}",
267 terminal_text(&context.source_endpoint)
268 ),
269 format!("fetched_by: {}", terminal_text(&context.fetched_by)),
270 format!("certified: {}", yes_no(context.certified)),
271 format!(
272 "point_in_time_guaranteed: {}",
273 yes_no(context.point_in_time_guaranteed)
274 ),
275 format!("query_call_count: {}", context.query_call_count),
276 ]
277}