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