1use crate::app::App;
2use crate::common::{render_pagination_text, CyclicEnum, InputFocus, SortDirection};
3use crate::fsx::fs;
4use crate::table::{TableState, TableWindow};
5use crate::tags::{self, TagSort, TagsConfig};
6use crate::ui::filter::{render_simple_filter, SimpleFilterConfig};
7use crate::ui::format_title;
8use crate::ui::render_tabs;
9use crate::ui::table::{expanded_from_columns, render_table, Column, TableConfig};
10use crate::ui::Summary;
11use ratatui::prelude::*;
12use ratatui::widgets::Paragraph;
13use rusticity_core::fsx::FsxFileSystem;
14
15pub const FILTER_CONTROLS: [InputFocus; 2] = [InputFocus::Filter, InputFocus::Pagination];
16
17pub const TAGS_COLUMNS: [&str; 2] = ["Key", "Value"];
19pub const UPDATES_COLUMNS: [&str; 5] = [
21 "Update type",
22 "Target value",
23 "Status",
24 "Progress %",
25 "Request time",
26];
27pub const BACKUPS_COLUMNS: [&str; 15] = [
29 "Backup name",
30 "Backup ID",
31 "File system type",
32 "Lifecycle state",
33 "Progress %",
34 "Type",
35 "Deployment Type",
36 "Storage class",
37 "Storage",
38 "Resource ID",
39 "Resource Name",
40 "Backup time",
41 "Active directory",
42 "File system Lustre version",
43 "KMS key ID",
44];
45
46pub fn detail_fixed_columns(detail_tab: DetailTab) -> Option<&'static [&'static str]> {
48 match detail_tab {
49 DetailTab::Tags => Some(&TAGS_COLUMNS),
50 DetailTab::Updates => Some(&UPDATES_COLUMNS),
51 DetailTab::Backups => Some(&BACKUPS_COLUMNS),
52 _ => None,
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq)]
58pub enum DetailTab {
59 NetworkSecurity,
60 MonitoringPerformance,
61 Administration,
62 DataRepository,
63 Backups,
64 Updates,
65 Tags,
66}
67
68impl CyclicEnum for DetailTab {
69 const ALL: &'static [Self] = &[
70 Self::NetworkSecurity,
71 Self::MonitoringPerformance,
72 Self::Administration,
73 Self::DataRepository,
74 Self::Backups,
75 Self::Updates,
76 Self::Tags,
77 ];
78}
79
80impl DetailTab {
81 pub fn name(&self) -> &'static str {
82 match self {
83 DetailTab::NetworkSecurity => "Network & security",
84 DetailTab::MonitoringPerformance => "Monitoring & performance",
85 DetailTab::Administration => "Administration",
86 DetailTab::DataRepository => "Data repository",
87 DetailTab::Backups => "Backups",
88 DetailTab::Updates => "Updates",
89 DetailTab::Tags => "Tags",
90 }
91 }
92}
93
94pub struct State {
95 pub file_systems: TableState<FsxFileSystem>,
96 pub input_focus: InputFocus,
97 pub current_file_system: Option<String>,
98 pub detail_tab: DetailTab,
99 pub tags_table: TableState<(String, String)>,
100 pub updates_table: TableState<rusticity_core::fsx::FsxUpdate>,
101 pub backups: TableState<rusticity_core::fsx::FsxBackup>,
102 pub backups_loading: bool,
103}
104
105impl Default for State {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl State {
112 pub fn new() -> Self {
113 Self {
114 file_systems: TableState::new(),
115 input_focus: InputFocus::Filter,
116 current_file_system: None,
117 detail_tab: DetailTab::NetworkSecurity,
118 tags_table: TableState::new(),
119 updates_table: TableState::new(),
120 backups: TableState::new(),
121 backups_loading: false,
122 }
123 }
124}
125
126pub fn filtered_fsx_file_systems(app: &App) -> Vec<&FsxFileSystem> {
127 let filter = app.fsx_state.file_systems.filter.to_lowercase();
128 app.fsx_state
129 .file_systems
130 .items
131 .iter()
132 .filter(|fs| {
133 filter.is_empty()
134 || fs.name.to_lowercase().contains(&filter)
135 || fs.file_system_id.to_lowercase().contains(&filter)
136 || fs.file_system_type.to_lowercase().contains(&filter)
137 })
138 .collect()
139}
140
141pub fn render_file_systems(frame: &mut Frame, app: &App, area: Rect) {
142 if app.fsx_state.current_file_system.is_some() {
143 render_file_system_detail(frame, app, area);
144 } else {
145 render_file_systems_list(frame, app, area);
146 }
147}
148
149pub fn format_deployment_type(raw: &str) -> String {
153 if let Some(last) = raw.chars().last() {
154 if last.is_ascii_digit() && raw.len() > 1 {
155 let (head, tail) = raw.split_at(raw.len() - 1);
156 return format!("{} {}", head, tail);
157 }
158 }
159 raw.to_string()
160}
161
162pub fn format_storage_class(raw: &str) -> String {
164 raw.to_uppercase()
165}
166
167pub fn format_compression(raw: &str) -> String {
169 raw.to_uppercase()
170}
171
172pub fn format_capacity_tb(gib: i64) -> String {
174 if gib <= 0 {
175 return "-".to_string();
176 }
177 format!("{:.1} TB", gib as f64 / 1000.0)
178}
179
180pub fn format_weekly_maintenance(raw: &str) -> String {
182 let parts: Vec<&str> = raw.split(':').collect();
183 if parts.len() == 3 {
184 let day = match parts[0] {
185 "1" => "Monday",
186 "2" => "Tuesday",
187 "3" => "Wednesday",
188 "4" => "Thursday",
189 "5" => "Friday",
190 "6" => "Saturday",
191 "7" => "Sunday",
192 _ => parts[0],
193 };
194 return format!("{} {}:{} UTC", day, parts[1], parts[2]);
195 }
196 raw.to_string()
197}
198
199pub fn enabled_disabled_color(value: &str) -> Color {
201 match value {
202 "Enabled" | "ENABLED" | "enabled" => Color::Green,
203 "Disabled" | "DISABLED" | "disabled" => Color::Yellow,
204 _ => Color::White,
205 }
206}
207
208fn lifecycle_color(status: &str) -> Color {
209 match status {
210 "AVAILABLE" | "Available" | "available" => Color::Green,
211 "CREATING" | "UPDATING" | "Creating" | "Updating" => Color::Yellow,
212 "FAILED" | "DELETING" | "MISCONFIGURED" | "MISCONFIGURED_UNAVAILABLE" => Color::Red,
213 _ => Color::White,
214 }
215}
216
217fn build_summary_lines(fs: &FsxFileSystem) -> Vec<Line<'static>> {
218 use crate::ui::{labeled_field, labeled_field_colored};
219
220 let mut lines: Vec<Line> = vec![
221 labeled_field("File system ID", fs.file_system_id.clone()),
222 labeled_field_colored(
223 "Lifecycle state",
224 fs.status.clone(),
225 lifecycle_color(&fs.status),
226 ),
227 labeled_field("File system type", fs.file_system_type.clone()),
228 ];
229 if !fs.deployment_type.is_empty() {
230 lines.push(labeled_field(
231 "Deployment type",
232 format_deployment_type(&fs.deployment_type),
233 ));
234 }
235 if !fs.data_compression_type.is_empty() {
236 lines.push(labeled_field(
237 "Data compression type",
238 format_compression(&fs.data_compression_type),
239 ));
240 }
241 if !fs.storage_class.is_empty() {
242 lines.push(labeled_field(
243 "Storage class",
244 format_storage_class(&fs.storage_class),
245 ));
246 }
247 lines.push(labeled_field(
248 "Storage capacity",
249 format_capacity_tb(fs.storage_capacity_gib),
250 ));
251 if fs.per_unit_throughput > 0 {
252 lines.push(labeled_field(
253 "Throughput per unit of storage",
254 format!("{} MB/s/TiB", fs.per_unit_throughput),
255 ));
256 }
257 if fs.total_throughput_mbps > 0 {
258 lines.push(labeled_field(
259 "Total throughput",
260 format!("{} MB/s", fs.total_throughput_mbps),
261 ));
262 }
263 if !fs.efa.is_empty() {
264 lines.push(labeled_field_colored(
265 "EFA",
266 fs.efa.clone(),
267 enabled_disabled_color(&fs.efa),
268 ));
269 }
270 if !fs.root_squash.is_empty() {
271 lines.push(labeled_field_colored(
272 "Root Squash",
273 fs.root_squash.clone(),
274 enabled_disabled_color(&fs.root_squash),
275 ));
276 }
277 if !fs.lustre_version.is_empty() {
278 lines.push(labeled_field("Lustre version", fs.lustre_version.clone()));
279 }
280 if !fs.subnet_ids.is_empty() {
281 lines.push(labeled_field("Subnet IDs", fs.subnet_ids.clone()));
282 }
283 lines.push(labeled_field("Creation time", fs.creation_time.clone()));
284 if !fs.mount_name.is_empty() {
285 lines.push(labeled_field("Mount name", fs.mount_name.clone()));
286 }
287 lines
288}
289
290pub fn render_file_system_detail(frame: &mut Frame, app: &App, area: Rect) {
291 let fs = app.fsx_state.current_file_system.as_deref().and_then(|id| {
292 app.fsx_state
293 .file_systems
294 .items
295 .iter()
296 .find(|f| f.file_system_id == id)
297 });
298
299 let tabs: Vec<(&str, DetailTab)> = DetailTab::ALL.iter().map(|t| (t.name(), *t)).collect();
300
301 let Some(fs) = fs else {
302 let chunks = Layout::default()
303 .direction(Direction::Vertical)
304 .constraints([Constraint::Min(0), Constraint::Length(1)])
305 .split(area);
306 render_tabs(frame, chunks[1], &tabs, &app.fsx_state.detail_tab);
307 return;
308 };
309
310 let summary = Summary::new("Summary", build_summary_lines(fs));
312 let summary_height = summary
313 .height(area.width)
314 .min(area.height.saturating_sub(2));
315
316 let chunks = Layout::default()
317 .direction(Direction::Vertical)
318 .constraints([
319 Constraint::Length(summary_height),
320 Constraint::Length(1),
321 Constraint::Min(0),
322 ])
323 .split(area);
324
325 summary.render(frame, chunks[0]);
326
327 render_tabs(frame, chunks[1], &tabs, &app.fsx_state.detail_tab);
328
329 match app.fsx_state.detail_tab {
330 DetailTab::NetworkSecurity => render_network_security(frame, fs, chunks[2]),
331 DetailTab::Administration => render_administration(frame, fs, chunks[2]),
332 DetailTab::Updates => render_updates_tab(frame, app, fs, chunks[2]),
333 DetailTab::Backups => render_backups_tab(frame, app, chunks[2]),
334 DetailTab::Tags => render_tags_tab(frame, app, fs, chunks[2]),
335 other => {
336 let msg = format!("{} — not yet implemented", other.name());
337 let p = Paragraph::new(msg)
338 .style(Style::default().fg(Color::DarkGray))
339 .alignment(Alignment::Center);
340 frame.render_widget(p, chunks[2]);
341 }
342 }
343}
344
345struct FieldCol<T: 'static> {
347 label: &'static str,
348 width: u16,
349 get: fn(&T) -> String,
350}
351
352impl<T> Column<T> for FieldCol<T> {
353 fn name(&self) -> &str {
354 self.label
355 }
356 fn width(&self) -> u16 {
357 self.width
358 }
359 fn render(&self, item: &T) -> (String, Style) {
360 ((self.get)(item), Style::default())
361 }
362}
363
364fn render_network_security(frame: &mut Frame, fs: &FsxFileSystem, area: Rect) {
365 use crate::ui::{labeled_field, render_fields_with_dynamic_columns};
366
367 let net_lines: Vec<Line> = vec![
368 labeled_field("VPC", fs.vpc_id.clone()),
369 labeled_field("DNS name", fs.dns_name.clone()),
370 labeled_field("Network interface IDs", fs.network_interface_ids.clone()),
371 labeled_field("KMS key ID", fs.kms_key_id.clone()),
372 ];
373 let subnet_lines: Vec<Line> = vec![
374 labeled_field("Subnet", fs.subnet_ids.clone()),
375 labeled_field("Availability Zone", "-"),
377 labeled_field("Network interface(s)", "See the Amazon EC2 Console"),
378 ];
379
380 let summary = Summary::new("Network & security", net_lines);
381 let h1 = summary.height(area.width).min(area.height);
382 let chunks = Layout::default()
383 .direction(Direction::Vertical)
384 .constraints([Constraint::Length(h1), Constraint::Min(0)])
385 .split(area);
386
387 summary.render(frame, chunks[0]);
388
389 let first_subnet = fs.subnet_ids.split(',').next().unwrap_or("").trim();
390 let subnet_title = if first_subnet.is_empty() {
391 " Subnet ".to_string()
392 } else {
393 format!(" Subnet ({}) ", first_subnet)
394 };
395 let block2 = crate::ui::titled_block(subnet_title);
396 let inner2 = block2.inner(chunks[1]);
397 frame.render_widget(block2, chunks[1]);
398 render_fields_with_dynamic_columns(frame, inner2, subnet_lines);
399}
400
401fn render_administration(frame: &mut Frame, fs: &FsxFileSystem, area: Rect) {
402 use crate::ui::{labeled_field, render_fields_with_dynamic_columns};
403
404 let mut lines: Vec<Line> = Vec::new();
405 if !fs.weekly_maintenance_start.is_empty() {
406 lines.push(labeled_field(
407 "Weekly maintenance window",
408 format_weekly_maintenance(&fs.weekly_maintenance_start),
409 ));
410 }
411 if fs.automatic_backup_retention_days > 0 {
412 lines.push(labeled_field(
413 "Automatic backup retention",
414 format!("{} days", fs.automatic_backup_retention_days),
415 ));
416 } else {
417 lines.push(labeled_field("Automatic backups", "Disabled"));
418 }
419 if !fs.daily_automatic_backup_start.is_empty() {
420 lines.push(labeled_field(
421 "Daily automatic backup window",
422 format!("{} UTC", fs.daily_automatic_backup_start),
423 ));
424 }
425
426 let block = crate::ui::titled_block(" Settings ");
427 let inner = block.inner(area);
428 frame.render_widget(block, area);
429 render_fields_with_dynamic_columns(frame, inner, lines);
430}
431
432fn render_updates_tab(frame: &mut Frame, app: &App, fs: &FsxFileSystem, area: Rect) {
433 use rusticity_core::fsx::FsxUpdate;
434
435 let rows: Vec<&FsxUpdate> = fs.updates.iter().collect();
436 let columns: Vec<Box<dyn Column<FsxUpdate>>> = vec![
437 Box::new(FieldCol {
438 label: "Update type",
439 width: 26,
440 get: |u: &FsxUpdate| u.update_type.clone(),
441 }),
442 Box::new(FieldCol {
443 label: "Target value",
444 width: 18,
445 get: |u: &FsxUpdate| u.target_value.clone(),
446 }),
447 Box::new(FieldCol {
448 label: "Status",
449 width: 18,
450 get: |u: &FsxUpdate| u.status.clone(),
451 }),
452 Box::new(FieldCol {
453 label: "Progress %",
454 width: 12,
455 get: |u: &FsxUpdate| u.progress_percent.clone(),
456 }),
457 Box::new(FieldCol {
458 label: "Request time",
459 width: 26,
460 get: |u: &FsxUpdate| u.request_time.clone(),
461 }),
462 ];
463
464 let selected = app
465 .fsx_state
466 .updates_table
467 .selected
468 .min(rows.len().saturating_sub(1));
469
470 let config = TableConfig {
471 items: rows,
472 selected_index: selected,
473 expanded_index: app.fsx_state.updates_table.expanded_item,
474 columns: &columns,
475 sort_column: "Request time",
476 sort_direction: SortDirection::Asc,
477 title: format_title(&format!("Updates ({})", fs.updates.len())),
478 area,
479 get_expanded_content: Some(Box::new(|u: &FsxUpdate| expanded_from_columns(&columns, u))),
480 is_active: app.mode != crate::keymap::Mode::FilterInput,
481 };
482 render_table(frame, config);
483}
484
485fn render_backups_tab(frame: &mut Frame, app: &App, area: Rect) {
486 use rusticity_core::fsx::FsxBackup;
487
488 if app.fsx_state.backups_loading {
489 let block = crate::ui::titled_block(" Backups ");
490 let inner = block.inner(area);
491 frame.render_widget(block, area);
492 let p = Paragraph::new("Loading backups...")
493 .style(Style::default().fg(Color::DarkGray))
494 .alignment(Alignment::Center);
495 frame.render_widget(p, inner);
496 return;
497 }
498
499 let rows: Vec<&FsxBackup> = app.fsx_state.backups.items.iter().collect();
500 let columns: Vec<Box<dyn Column<FsxBackup>>> = vec![
501 Box::new(FieldCol {
502 label: "Backup name",
503 width: 22,
504 get: |b: &FsxBackup| b.name.clone(),
505 }),
506 Box::new(FieldCol {
507 label: "Backup ID",
508 width: 24,
509 get: |b: &FsxBackup| b.backup_id.clone(),
510 }),
511 Box::new(FieldCol {
512 label: "File system type",
513 width: 16,
514 get: |b: &FsxBackup| b.file_system_type.clone(),
515 }),
516 Box::new(FieldCol {
517 label: "Lifecycle state",
518 width: 16,
519 get: |b: &FsxBackup| b.lifecycle_state.clone(),
520 }),
521 Box::new(FieldCol {
522 label: "Progress %",
523 width: 12,
524 get: |b: &FsxBackup| b.progress_percent.clone(),
525 }),
526 Box::new(FieldCol {
527 label: "Type",
528 width: 16,
529 get: |b: &FsxBackup| b.backup_type.clone(),
530 }),
531 Box::new(FieldCol {
532 label: "Deployment Type",
533 width: 18,
534 get: |b: &FsxBackup| b.deployment_type.clone(),
535 }),
536 Box::new(FieldCol {
537 label: "Storage class",
538 width: 13,
539 get: |b: &FsxBackup| b.storage_class.clone(),
540 }),
541 Box::new(FieldCol {
542 label: "Storage",
543 width: 14,
544 get: |b: &FsxBackup| b.storage.clone(),
545 }),
546 Box::new(FieldCol {
547 label: "Resource ID",
548 width: 22,
549 get: |b: &FsxBackup| b.resource_id.clone(),
550 }),
551 Box::new(FieldCol {
552 label: "Resource Name",
553 width: 22,
554 get: |b: &FsxBackup| b.resource_name.clone(),
555 }),
556 Box::new(FieldCol {
557 label: "Backup time",
558 width: 26,
559 get: |b: &FsxBackup| b.backup_time.clone(),
560 }),
561 Box::new(FieldCol {
562 label: "Active directory",
563 width: 18,
564 get: |b: &FsxBackup| b.active_directory.clone(),
565 }),
566 Box::new(FieldCol {
567 label: "File system Lustre version",
568 width: 26,
569 get: |b: &FsxBackup| b.lustre_version.clone(),
570 }),
571 Box::new(FieldCol {
572 label: "KMS key ID",
573 width: 30,
574 get: |b: &FsxBackup| b.kms_key_id.clone(),
575 }),
576 ];
577
578 let selected = app
579 .fsx_state
580 .backups
581 .selected
582 .min(rows.len().saturating_sub(1));
583
584 let config = TableConfig {
585 items: rows,
586 selected_index: selected,
587 expanded_index: app.fsx_state.backups.expanded_item,
588 columns: &columns,
589 sort_column: "Backup time",
590 sort_direction: SortDirection::Desc,
591 title: format_title(&format!("Backups ({})", app.fsx_state.backups.items.len())),
592 area,
593 get_expanded_content: Some(Box::new(|b: &FsxBackup| expanded_from_columns(&columns, b))),
594 is_active: app.mode != crate::keymap::Mode::FilterInput,
595 };
596 render_table(frame, config);
597}
598
599fn render_tags_tab(frame: &mut Frame, app: &App, fs: &FsxFileSystem, area: Rect) {
600 tags::render(
601 frame,
602 area,
603 TagsConfig {
604 table: &app.fsx_state.tags_table,
605 items: &fs.tags,
606 columns: tags::columns(),
607 sort: TagSort::Key,
608 window: crate::table::TableWindow::SelectedPage,
609 focus: app.fsx_state.input_focus,
610 mode: app.mode,
611 loading: false,
612 },
613 );
614}
615
616pub fn render_file_systems_list(frame: &mut Frame, app: &App, area: Rect) {
617 let chunks = Layout::default()
618 .direction(Direction::Vertical)
619 .constraints([Constraint::Length(3), Constraint::Min(0)])
620 .split(area);
621
622 let view = app
623 .fsx_state
624 .file_systems
625 .prepare_view(filtered_fsx_file_systems(app), TableWindow::SelectedPage);
626 let pagination = render_pagination_text(view.current_page, view.total_pages);
627
628 render_simple_filter(
629 frame,
630 chunks[0],
631 SimpleFilterConfig {
632 filter_text: &app.fsx_state.file_systems.filter,
633 placeholder: "Search by name, file system ID, or type",
634 pagination: &pagination,
635 mode: app.mode,
636 is_input_focused: app.fsx_state.input_focus == InputFocus::Filter,
637 is_pagination_focused: app.fsx_state.input_focus == InputFocus::Pagination,
638 },
639 );
640
641 let title = format_title(&format!("File Systems ({})", view.total_items));
642
643 let columns: Vec<Box<dyn Column<FsxFileSystem>>> = app
644 .fsx_visible_column_ids
645 .iter()
646 .filter_map(|col_id| {
647 fs::Column::from_id(col_id).map(|col| Box::new(col) as Box<dyn Column<FsxFileSystem>>)
648 })
649 .collect();
650
651 let config = TableConfig {
652 items: view.items,
653 selected_index: view.selected_index,
654 expanded_index: view.expanded_index,
655 columns: &columns,
656 sort_column: "Creation time",
657 sort_direction: SortDirection::Desc,
658 title,
659 area: chunks[1],
660 get_expanded_content: Some(Box::new(|fs: &FsxFileSystem| {
661 expanded_from_columns(&columns, fs)
662 })),
663 is_active: app.mode != crate::keymap::Mode::FilterInput,
664 };
665
666 render_table(frame, config);
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672 use crate::app::App;
673 use rusticity_core::fsx::FsxFileSystem;
674
675 fn make_fs(id: &str, name: &str, fstype: &str) -> FsxFileSystem {
676 FsxFileSystem {
677 name: name.to_string(),
678 file_system_id: id.to_string(),
679 file_system_type: fstype.to_string(),
680 status: "AVAILABLE".to_string(),
681 ..Default::default()
682 }
683 }
684
685 #[test]
686 fn test_state_default() {
687 let state = State::new();
688 assert_eq!(state.input_focus, InputFocus::Filter);
689 assert!(state.file_systems.items.is_empty());
690 assert_eq!(state.current_file_system, None);
691 assert_eq!(state.detail_tab, DetailTab::NetworkSecurity);
692 }
693
694 #[test]
695 fn test_filtered_by_name_id_type() {
696 let mut app = App::new_without_client("default".to_string(), None);
697 app.fsx_state.file_systems.items = vec![
698 make_fs("fs-001", "prod-lustre", "LUSTRE"),
699 make_fs("fs-002", "dev-windows", "WINDOWS"),
700 ];
701
702 app.fsx_state.file_systems.filter = "prod".to_string();
703 assert_eq!(filtered_fsx_file_systems(&app).len(), 1);
704
705 app.fsx_state.file_systems.filter = "fs-002".to_string();
706 assert_eq!(filtered_fsx_file_systems(&app).len(), 1);
707
708 app.fsx_state.file_systems.filter = "windows".to_string();
709 assert_eq!(filtered_fsx_file_systems(&app).len(), 1);
710
711 app.fsx_state.file_systems.filter = String::new();
712 assert_eq!(filtered_fsx_file_systems(&app).len(), 2);
713 }
714
715 #[test]
716 fn test_format_deployment_type() {
717 assert_eq!(format_deployment_type("Persistent2"), "Persistent 2");
718 assert_eq!(format_deployment_type("Persistent1"), "Persistent 1");
719 assert_eq!(format_deployment_type("Scratch2"), "Scratch 2");
720 assert_eq!(format_deployment_type("MultiAz"), "MultiAz");
722 }
723
724 #[test]
725 fn test_format_storage_class_and_compression() {
726 assert_eq!(format_storage_class("Ssd"), "SSD");
727 assert_eq!(format_storage_class("Hdd"), "HDD");
728 assert_eq!(format_compression("Lz4"), "LZ4");
729 assert_eq!(format_compression("None"), "NONE");
730 }
731
732 #[test]
733 fn test_format_capacity_tb() {
734 assert_eq!(format_capacity_tb(309600), "309.6 TB");
735 assert_eq!(format_capacity_tb(1200), "1.2 TB");
736 assert_eq!(format_capacity_tb(0), "-");
737 }
738
739 #[test]
740 fn test_detail_tab_cycles_through_seven() {
741 let mut tab = DetailTab::NetworkSecurity;
742 let mut seen = vec![tab];
743 for _ in 0..6 {
744 tab = tab.next();
745 seen.push(tab);
746 }
747 assert_eq!(seen.len(), 7);
748 assert_eq!(tab.next(), DetailTab::NetworkSecurity); assert_eq!(DetailTab::ALL.len(), 7);
750 }
751
752 #[test]
753 fn test_summary_includes_lustre_fields_when_present() {
754 let mut fs = make_fs("fs-abc", "kirin", "Lustre");
755 fs.deployment_type = "Persistent2".to_string();
756 fs.data_compression_type = "Lz4".to_string();
757 fs.storage_class = "Ssd".to_string();
758 fs.storage_capacity_gib = 309600;
759 fs.per_unit_throughput = 1000;
760 fs.total_throughput_mbps = 309600;
761 fs.efa = "Disabled".to_string();
762 fs.root_squash = "Disabled".to_string();
763 fs.lustre_version = "2.12".to_string();
764 fs.mount_name = "v4kavb4v".to_string();
765
766 let lines = build_summary_lines(&fs);
767 let text: String = lines
768 .iter()
769 .flat_map(|l| l.spans.iter().map(|s| s.content.to_string()))
770 .collect::<Vec<_>>()
771 .join("");
772 assert!(text.contains("Persistent 2"));
773 assert!(text.contains("LZ4"));
774 assert!(text.contains("SSD"));
775 assert!(text.contains("309.6 TB"));
776 assert!(text.contains("1000 MB/s/TiB"));
777 assert!(text.contains("309600 MB/s"));
778 assert!(text.contains("2.12"));
779 assert!(text.contains("v4kavb4v"));
780 }
781
782 #[test]
783 fn test_column_count_uses_tags_columns_on_tags_tab() {
784 let mut app = App::new_without_client("default".to_string(), None);
787 app.fsx_state.file_systems.items = vec![make_fs("fs-1", "n", "LUSTRE")];
788
789 assert_eq!(crate::fsx::actions::column_count(&app), 13);
791
792 app.fsx_state.current_file_system = Some("fs-1".to_string());
794 app.fsx_state.detail_tab = DetailTab::Tags;
795 assert_eq!(crate::fsx::actions::column_count(&app), 2);
796 assert_eq!(crate::fsx::actions::column_selector_max(&app), 2 + 6);
797
798 app.fsx_state.detail_tab = DetailTab::NetworkSecurity;
800 assert_eq!(crate::fsx::actions::column_count(&app), 13);
801 }
802
803 #[test]
804 fn test_enabled_disabled_color() {
805 assert_eq!(enabled_disabled_color("Enabled"), Color::Green);
806 assert_eq!(enabled_disabled_color("Disabled"), Color::Yellow);
807 assert_eq!(enabled_disabled_color("other"), Color::White);
808 }
809
810 #[test]
811 fn test_summary_omits_lustre_fields_for_windows() {
812 let mut fs = make_fs("fs-win", "winfs", "Windows");
813 fs.deployment_type = "MultiAz".to_string();
814 fs.storage_class = "Ssd".to_string();
815 fs.storage_capacity_gib = 1024;
816 let lines = build_summary_lines(&fs);
817 let text: String = lines
818 .iter()
819 .flat_map(|l| l.spans.iter().map(|s| s.content.to_string()))
820 .collect::<Vec<_>>()
821 .join("");
822 assert!(!text.contains("Lustre version"));
823 assert!(!text.contains("Mount name"));
824 assert!(!text.contains("MB/s/TiB"));
825 }
826
827 #[test]
828 fn test_format_weekly_maintenance() {
829 assert_eq!(format_weekly_maintenance("4:07:00"), "Thursday 07:00 UTC");
830 assert_eq!(format_weekly_maintenance("1:00:00"), "Monday 00:00 UTC");
831 assert_eq!(format_weekly_maintenance("7:23:30"), "Sunday 23:30 UTC");
832 assert_eq!(format_weekly_maintenance("weird"), "weird");
834 }
835
836 #[test]
837 fn test_detail_fixed_columns() {
838 assert_eq!(
839 detail_fixed_columns(DetailTab::Tags),
840 Some(&TAGS_COLUMNS[..])
841 );
842 assert_eq!(
843 detail_fixed_columns(DetailTab::Updates),
844 Some(&UPDATES_COLUMNS[..])
845 );
846 assert_eq!(
847 detail_fixed_columns(DetailTab::Backups),
848 Some(&BACKUPS_COLUMNS[..])
849 );
850 assert_eq!(detail_fixed_columns(DetailTab::NetworkSecurity), None);
851 assert_eq!(detail_fixed_columns(DetailTab::Administration), None);
852 assert_eq!(UPDATES_COLUMNS.len(), 5);
854 assert_eq!(BACKUPS_COLUMNS.len(), 15);
855 }
856
857 #[test]
858 fn test_column_count_per_detail_tab() {
859 let mut app = App::new_without_client("default".to_string(), None);
860 app.fsx_state.file_systems.items = vec![make_fs("fs-1", "n", "LUSTRE")];
861 assert_eq!(crate::fsx::actions::column_count(&app), 13);
863
864 app.fsx_state.current_file_system = Some("fs-1".to_string());
865 app.fsx_state.detail_tab = DetailTab::Tags;
866 assert_eq!(crate::fsx::actions::column_count(&app), 2);
867 app.fsx_state.detail_tab = DetailTab::Updates;
868 assert_eq!(crate::fsx::actions::column_count(&app), 5);
869 app.fsx_state.detail_tab = DetailTab::Backups;
870 assert_eq!(crate::fsx::actions::column_count(&app), 15);
871 app.fsx_state.detail_tab = DetailTab::NetworkSecurity;
873 assert_eq!(crate::fsx::actions::column_count(&app), 13);
874 }
875}