1use colored::Colorize;
2use comfy_table::Table;
3use wasmer_backend_api::types::{
4 CronJob, CronJobInvocation, CronJobInvocationResult, CronJobKind, CronJobLog, CronJobTarget,
5 DeployApp, DeployAppVersion, Deployment, DnsDomain, DnsDomainWithRecords, LogStream, Namespace,
6 SearchPackageVersion,
7};
8
9use crate::utils::render::CliRender;
10
11fn package_full_name(pv: &SearchPackageVersion) -> String {
13 match &pv.package.namespace {
14 Some(ns) => format!("{ns}/{}", pv.package.package_name),
15 None => pv.package.package_name.clone(),
16 }
17}
18
19fn cron_job_target_summary(target: &CronJobTarget) -> String {
20 match target {
21 CronJobTarget::FetchCronJobTarget(target) => {
22 format!("{} {}", target.method, target.path)
23 }
24 CronJobTarget::ExecuteCronJobTarget(target) => {
25 let package = target.package_name.as_deref().unwrap_or("-");
26 let command = target.command.as_deref().unwrap_or("-");
27 format!("{package} {command}")
28 }
29 CronJobTarget::Unknown => "unknown".to_string(),
30 }
31}
32
33fn cron_job_kind(kind: CronJobKind) -> &'static str {
34 match kind {
35 CronJobKind::Fetch => "fetch",
36 CronJobKind::Execute => "execute",
37 }
38}
39
40fn cron_invocation_result_summary(result: &Option<CronJobInvocationResult>) -> String {
41 match result {
42 Some(CronJobInvocationResult::ExecuteCronJobInvocationResult(result)) => result
43 .exit_code
44 .map(|code| format!("exit_code={code}"))
45 .unwrap_or_else(|| "execute".to_string()),
46 Some(CronJobInvocationResult::FetchCronJobInvocationResult(result)) => result
47 .status_code
48 .map(|code| format!("status_code={code}"))
49 .unwrap_or_else(|| "fetch".to_string()),
50 Some(CronJobInvocationResult::Unknown) => "unknown".to_string(),
51 None => "-".to_string(),
52 }
53}
54
55impl CliRender for CronJob {
56 fn render_item_table(&self) -> String {
57 let mut table = Table::new();
58 table.add_rows([
59 vec!["Name".to_string(), self.name.clone()],
60 vec!["Id".to_string(), self.id.inner().to_string()],
61 vec!["Kind".to_string(), cron_job_kind(self.kind).to_string()],
62 vec!["Schedule".to_string(), self.schedule.clone()],
63 vec!["Enabled".to_string(), self.enabled.to_string()],
64 vec!["Managed".to_string(), self.is_managed.to_string()],
65 vec!["Target".to_string(), cron_job_target_summary(&self.target)],
66 vec![
67 "Max retries".to_string(),
68 self.max_retries
69 .map(|value| value.to_string())
70 .unwrap_or_else(|| "-".to_string()),
71 ],
72 vec![
73 "Timeout".to_string(),
74 self.timeout.clone().unwrap_or_else(|| "-".to_string()),
75 ],
76 vec!["Created".to_string(), self.created_at.0.clone()],
77 vec!["Updated".to_string(), self.updated_at.0.clone()],
78 ]);
79 table.to_string()
80 }
81
82 fn render_list_table(items: &[Self]) -> String {
83 let mut table = Table::new();
84 table.set_header(vec![
85 "Name".to_string(),
86 "Kind".to_string(),
87 "Schedule".to_string(),
88 "Enabled".to_string(),
89 "Target".to_string(),
90 "Id".to_string(),
91 ]);
92 table.add_rows(items.iter().map(|job| {
93 vec![
94 job.name.clone(),
95 cron_job_kind(job.kind).to_string(),
96 job.schedule.clone(),
97 job.enabled.to_string(),
98 cron_job_target_summary(&job.target),
99 job.id.inner().to_string(),
100 ]
101 }));
102 table.to_string()
103 }
104}
105
106impl CliRender for CronJobInvocation {
107 fn render_item_table(&self) -> String {
108 let mut table = Table::new();
109 table.add_rows([
110 vec!["Id".to_string(), self.id.inner().to_string()],
111 vec![
112 "Status".to_string(),
113 self.status
114 .map(|status| format!("{status:?}"))
115 .unwrap_or_else(|| "-".to_string()),
116 ],
117 vec![
118 "Scheduled".to_string(),
119 self.scheduled_at
120 .as_ref()
121 .map(|value| value.0.clone())
122 .unwrap_or_else(|| "-".to_string()),
123 ],
124 vec![
125 "Started".to_string(),
126 self.started_at
127 .as_ref()
128 .map(|value| value.0.clone())
129 .unwrap_or_else(|| "-".to_string()),
130 ],
131 vec![
132 "Finished".to_string(),
133 self.finished_at
134 .as_ref()
135 .map(|value| value.0.clone())
136 .unwrap_or_else(|| "-".to_string()),
137 ],
138 vec![
139 "Duration ms".to_string(),
140 self.duration_ms
141 .map(|value| value.to_string())
142 .unwrap_or_else(|| "-".to_string()),
143 ],
144 vec![
145 "Result".to_string(),
146 cron_invocation_result_summary(&self.result),
147 ],
148 vec![
149 "Error".to_string(),
150 self.error_summary
151 .clone()
152 .unwrap_or_else(|| "-".to_string()),
153 ],
154 ]);
155 table.to_string()
156 }
157
158 fn render_list_table(items: &[Self]) -> String {
159 let mut table = Table::new();
160 table.set_header(vec![
161 "Id".to_string(),
162 "Status".to_string(),
163 "Scheduled".to_string(),
164 "Started".to_string(),
165 "Duration ms".to_string(),
166 "Result".to_string(),
167 ]);
168 table.add_rows(items.iter().map(|invocation| {
169 vec![
170 invocation.id.inner().to_string(),
171 invocation
172 .status
173 .map(|status| format!("{status:?}"))
174 .unwrap_or_else(|| "-".to_string()),
175 invocation
176 .scheduled_at
177 .as_ref()
178 .map(|value| value.0.clone())
179 .unwrap_or_else(|| "-".to_string()),
180 invocation
181 .started_at
182 .as_ref()
183 .map(|value| value.0.clone())
184 .unwrap_or_else(|| "-".to_string()),
185 invocation
186 .duration_ms
187 .map(|value| value.to_string())
188 .unwrap_or_else(|| "-".to_string()),
189 cron_invocation_result_summary(&invocation.result),
190 ]
191 }));
192 table.to_string()
193 }
194}
195
196impl CliRender for CronJobLog {
197 fn render_item_table(&self) -> String {
198 let mut table = Table::new();
199 table.add_rows([
200 vec!["Timestamp".to_string(), self.datetime.0.clone()],
201 vec!["Message".to_string(), self.message.clone()],
202 ]);
203 table.to_string()
204 }
205
206 fn render_list_table(items: &[Self]) -> String {
207 let mut table = Table::new();
208 table.load_style(comfy_table::presets::NOTHING);
209 table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
210
211 for item in items {
212 let message = match item.stream {
213 Some(LogStream::Stderr) => item.message.clone().yellow(),
214 Some(LogStream::Runtime) => item.message.clone().cyan(),
215 Some(LogStream::Stdout) | None => item.message.clone().bold(),
216 };
217 table.add_row([
218 comfy_table::Cell::new(format!("[{}]", item.datetime.0))
219 .set_alignment(comfy_table::CellAlignment::Right),
220 comfy_table::Cell::new(message),
221 ]);
222 }
223 table.to_string()
224 }
225}
226
227impl CliRender for SearchPackageVersion {
228 fn render_item_table(&self) -> String {
229 let mut table = Table::new();
230 table.add_rows([
231 vec!["Package".to_string(), package_full_name(self)],
232 vec!["Version".to_string(), self.version.clone()],
233 vec!["Created".to_string(), self.created_at.0.clone()],
234 ]);
235 table.to_string()
236 }
237
238 fn render_list_table(items: &[Self]) -> String {
239 let mut table = Table::new();
240 table.set_header(vec![
241 "Package".to_string(),
242 "Version".to_string(),
243 "Created".to_string(),
244 ]);
245 table.add_rows(items.iter().map(|pv| {
246 vec![
247 package_full_name(pv),
248 pv.version.clone(),
249 pv.created_at.0.clone(),
250 ]
251 }));
252 table.to_string()
253 }
254}
255
256impl CliRender for DnsDomain {
257 fn render_item_table(&self) -> String {
258 let mut table = Table::new();
259 table.add_rows([vec!["Domain".to_string(), self.name.clone()]]);
260 table.to_string()
261 }
262
263 fn render_list_table(items: &[Self]) -> String {
264 if items.is_empty() {
265 return String::new();
266 }
267 let mut table = Table::new();
268 table.set_header(vec!["Domain".to_string(), "Owner".to_string()]);
269 table.add_rows(
270 items
271 .iter()
272 .map(|ns| vec![ns.name.clone(), ns.owner.global_name.clone()]),
273 );
274 table.to_string()
275 }
276}
277
278impl CliRender for DnsDomainWithRecords {
279 fn render_item_table(&self) -> String {
280 let mut output = String::new();
281 let mut table = Table::new();
282 table
283 .load_style(comfy_table::presets::UTF8_FULL_CONDENSED)
284 .set_header(vec![
285 "Type".to_string(),
286 "Name".to_string(),
287 "TTL".to_string(),
288 "Value".to_string(),
289 ]);
290 let mut rows: Vec<Vec<String>> = vec![];
291 if let Some(ref records) = self.records {
292 records.iter().flatten().for_each(|record| {
293 rows.push(vec![
294 record.record_type().to_string(),
295 record.name().unwrap_or("<no name>").to_string(),
296 record
297 .ttl()
298 .expect("expected a TTL value for record")
299 .to_string(),
300 record.text().to_string(),
301 ]);
302 });
303 }
304
305 table.add_rows(rows);
306 output += &table.to_string();
307 output
308 }
309
310 fn render_list_table(items: &[Self]) -> String {
311 if items.is_empty() {
312 return String::new();
313 }
314 let mut table = Table::new();
315 table.set_header(vec!["Domain".to_string()]);
316 table.add_rows(items.iter().map(|ns| vec![ns.name.clone()]));
317 table.to_string()
318 }
319}
320
321impl CliRender for Namespace {
322 fn render_item_table(&self) -> String {
323 let mut table = Table::new();
324 table.add_rows([
325 vec!["Namespace".to_string(), self.name.clone()],
326 vec!["Id".to_string(), self.id.inner().to_string()],
327 ]);
328 table.to_string()
329 }
330
331 fn render_list_table(items: &[Self]) -> String {
332 let mut table = Table::new();
333 table.set_header(vec!["Namespace".to_string(), "Id".to_string()]);
334 table.add_rows(
335 items
336 .iter()
337 .map(|ns| vec![ns.name.clone(), ns.id.inner().to_string()]),
338 );
339 table.to_string()
340 }
341}
342
343impl CliRender for DeployApp {
344 fn render_item_table(&self) -> String {
345 let mut table = Table::new();
346 table.add_rows([
347 vec![
348 "App".to_string(),
349 format!("{}/{}", self.owner.global_name, self.name),
350 ],
351 vec![
352 "Version".to_string(),
353 self.active_version
354 .as_ref()
355 .map_or_else(|| "n/a".to_string(), |v| v.version.clone()),
356 ],
357 vec!["Created".to_string(), self.created_at.0.clone()],
358 vec!["Updated".to_string(), self.updated_at.0.clone()],
359 vec!["Id".to_string(), self.id.inner().to_string()],
360 ]);
361 table.to_string()
362 }
363
364 fn render_list_table(items: &[Self]) -> String {
365 let mut table = Table::new();
366 table.set_header(vec![
367 "App".to_string(),
368 "Version".to_string(),
369 "Created".to_string(),
370 "Updated".to_string(),
371 "Id".to_string(),
372 ]);
373 table.add_rows(items.iter().map(|app| {
374 vec![
375 format!("{}/{}", app.owner.global_name, app.name),
376 app.active_version
377 .as_ref()
378 .map_or_else(|| "n/a".to_string(), |v| v.version.clone()),
379 app.created_at.0.clone(),
380 app.updated_at.0.clone(),
381 app.id.inner().to_string(),
382 ]
383 }));
384 table.to_string()
385 }
386}
387
388impl CliRender for DeployAppVersion {
389 fn render_item_table(&self) -> String {
390 let mut table = Table::new();
391 table.add_rows([
392 vec!["Version name".to_string(), self.version.clone()],
393 vec!["Created".to_string(), self.created_at.0.clone()],
394 vec!["Id".to_string(), self.id.inner().to_string()],
395 ]);
396 table.to_string()
397 }
398
399 fn render_list_table(items: &[Self]) -> String {
400 let mut table = Table::new();
401 table.set_header(vec![
402 "Version name".to_string(),
403 "Created".to_string(),
404 "Id".to_string(),
405 ]);
406 table.add_rows(items.iter().map(|ver| {
407 vec![
408 ver.version.clone(),
409 ver.created_at.0.clone(),
410 ver.id.inner().to_string(),
411 ]
412 }));
413 table.to_string()
414 }
415}
416
417impl CliRender for wasmer_backend_api::types::AppDatabase {
418 fn render_item_table(&self) -> String {
419 let mut table = Table::new();
420 table.add_rows([
421 vec!["Name".to_string(), self.name.clone()],
422 vec!["Host".to_string(), self.host.clone()],
423 vec!["Port".to_string(), self.port.clone()],
424 vec!["Username".to_string(), self.username.clone()],
425 vec![
426 "Password".to_string(),
427 self.password.clone().unwrap_or_else(|| "n/a".to_string()),
428 ],
429 vec![
430 "UI".to_string(),
431 self.db_explorer_url
432 .clone()
433 .unwrap_or_else(|| "n/a".to_string()),
434 ],
435 ]);
436 table.to_string()
437 }
438
439 fn render_list_table(items: &[Self]) -> String {
440 let mut table = Table::new();
441 table.set_header(vec![
442 "Name".to_string(),
443 "Host".to_string(),
444 "Port".to_string(),
445 "UI".to_string(),
446 "Password".to_string(),
447 ]);
448 table.add_rows(items.iter().map(|vol| {
449 vec![
450 vol.name.clone(),
451 vol.host.clone(),
452 vol.port.clone(),
453 vol.db_explorer_url
454 .clone()
455 .unwrap_or_else(|| "n/a".to_string()),
456 vol.password.clone().unwrap_or_else(|| "n/a".to_string()),
457 ]
458 }));
459 table.to_string()
460 }
461}
462
463pub(crate) fn format_disk_size_opt(value: Option<wasmer_backend_api::types::BigInt>) -> String {
464 let value = value.and_then(|x| {
465 let y: Option<u64> = x.0.try_into().ok();
466 y
467 });
468
469 if let Some(v) = value {
470 let s = bytesize::ByteSize(v);
471 s.to_string()
472 } else {
473 "n/a".to_string()
474 }
475}
476
477impl CliRender for Deployment {
478 fn render_item_table(&self) -> String {
479 match self {
480 Deployment::NakedDeployment(naked) => naked.render_item_table(),
481 Deployment::AutobuildRepository(build) => build.render_item_table(),
482 Deployment::Other => "unknown deployment type".to_string(),
483 }
484 }
485
486 fn render_list_table(items: &[Self]) -> String {
487 let mut table = Table::new();
488 table.set_header(vec![
489 "Id".to_string(),
490 "Type".to_string(),
491 "Created at".to_string(),
492 "Status".to_string(),
493 "App version".to_string(),
494 ]);
495
496 let rows = items
497 .iter()
498 .map(|item| match item {
499 Deployment::NakedDeployment(naked) => {
500 vec![
501 naked.id.inner().to_string(),
502 "Manual".to_string(),
503 naked.created_at.0.clone(),
504 String::new(),
505 naked
506 .app_version
507 .as_ref()
508 .map_or_else(|| "n/a".to_string(), |x| x.version.clone()),
509 ]
510 }
511 Deployment::AutobuildRepository(build) => {
512 vec![
513 build.id.inner().to_string(),
514 "Autobuild".to_string(),
515 build.status.as_str().to_string(),
516 build.created_at.0.clone(),
517 ]
518 }
519 Deployment::Other => vec![
520 String::new(),
521 "Unknown".to_string(),
522 String::new(),
523 String::new(),
524 ],
525 })
526 .collect::<Vec<_>>();
527 table.add_rows(rows);
528
529 table.to_string()
530 }
531}
532
533impl CliRender for wasmer_backend_api::types::NakedDeployment {
534 fn render_item_table(&self) -> String {
535 let mut table = Table::new();
536 table.add_rows([
537 vec!["Id".to_string(), self.id.clone().into_inner()],
538 vec!["Created at".to_string(), self.created_at.0.clone()],
539 vec![
540 "App version".to_string(),
541 self.app_version
542 .as_ref()
543 .map_or_else(|| "n/a".to_string(), |x| x.version.clone()),
544 ],
545 ]);
546 table.to_string()
547 }
548
549 fn render_list_table(items: &[Self]) -> String {
550 let mut table = Table::new();
551 table.set_header(vec![
552 "Id".to_string(),
553 "Created at".to_string(),
554 "App version".to_string(),
555 ]);
556 table.add_rows(items.iter().map(|item| {
557 vec![
558 item.id.clone().into_inner(),
559 item.created_at.0.clone(),
560 item.app_version
561 .as_ref()
562 .map_or_else(|| "n/a".to_string(), |x| x.version.clone()),
563 ]
564 }));
565 table.to_string()
566 }
567}
568
569impl CliRender for wasmer_backend_api::types::AutobuildRepository {
570 fn render_item_table(&self) -> String {
571 let mut table = Table::new();
572 table.add_rows([
573 vec!["Id".to_string(), self.id.clone().into_inner()],
574 vec!["Status".to_string(), self.status.as_str().to_string()],
575 vec!["Created at".to_string(), self.created_at.0.clone()],
576 ]);
577 table.to_string()
578 }
579
580 fn render_list_table(items: &[Self]) -> String {
581 let mut table = Table::new();
582 table.set_header(vec![
583 "Id".to_string(),
584 "Status".to_string(),
585 "Created at".to_string(),
586 ]);
587 table.add_rows(items.iter().map(|item| {
588 vec![
589 item.id.clone().into_inner(),
590 item.status.as_str().to_string(),
591 item.created_at.0.clone(),
592 ]
593 }));
594 table.to_string()
595 }
596}