• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kdash-rs / kdash / 10584125856

27 Aug 2024 06:52PM UTC coverage: 36.747% (+1.0%) from 35.735%
10584125856

push

github

deepu105
fix tests

1257 of 6387 branches covered (19.68%)

Branch coverage included in aggregate %.

3286 of 5976 relevant lines covered (54.99%)

8.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

41.35
/src/ui/utils.rs
1
use std::{collections::BTreeMap, rc::Rc, sync::OnceLock};
2

3
use glob_match::glob_match;
4
use ratatui::{
5
  layout::{Constraint, Direction, Layout, Rect},
6
  style::{Color, Modifier, Style},
7
  symbols,
8
  text::{Line, Span, Text},
9
  widgets::{Block, Borders, Paragraph, Row, Table, Wrap},
10
  Frame,
11
};
12
use serde::Serialize;
13

14
use super::HIGHLIGHT;
15
use crate::app::{
16
  models::{KubeResource, StatefulTable},
17
  ActiveBlock, App,
18
};
19
// Utils
20

21
pub static COPY_HINT: &str = "| copy <c>";
22
pub static DESCRIBE_AND_YAML_HINT: &str = "| describe <d> | yaml <y> ";
23
pub static DESCRIBE_YAML_AND_ESC_HINT: &str = "| describe <d> | yaml <y> | back to menu <esc> ";
24
pub static DESCRIBE_YAML_DECODE_AND_ESC_HINT: &str =
25
  "| describe <d> | yaml <y> | decode <x> | back to menu <esc> ";
26

27
// default colors
28
pub const COLOR_TEAL: Color = Color::Rgb(35, 50, 55);
29
pub const COLOR_CYAN: Color = Color::Rgb(0, 230, 230);
30
pub const COLOR_LIGHT_BLUE: Color = Color::Rgb(138, 196, 255);
31
pub const COLOR_YELLOW: Color = Color::Rgb(249, 229, 113);
32
pub const COLOR_GREEN: Color = Color::Rgb(72, 213, 150);
33
pub const COLOR_RED: Color = Color::Rgb(249, 167, 164);
34
pub const COLOR_ORANGE: Color = Color::Rgb(255, 170, 66);
35
pub const COLOR_WHITE: Color = Color::Rgb(255, 255, 255);
36
pub const COLOR_MAGENTA: Color = Color::Rgb(199, 146, 234);
37
pub const COLOR_DARK_GRAY: Color = Color::Rgb(50, 50, 50);
38
// light theme colors
39
pub const COLOR_MAGENTA_DARK: Color = Color::Rgb(153, 26, 237);
40
pub const COLOR_GRAY: Color = Color::Rgb(91, 87, 87);
41
pub const COLOR_BLUE: Color = Color::Rgb(0, 82, 163);
42
pub const COLOR_GREEN_DARK: Color = Color::Rgb(20, 97, 73);
43
pub const COLOR_RED_DARK: Color = Color::Rgb(173, 25, 20);
44
pub const COLOR_ORANGE_DARK: Color = Color::Rgb(184, 49, 15);
45
// YAML background colors
46
const YAML_BACKGROUND_LIGHT: syntect::highlighting::Color = syntect::highlighting::Color::WHITE;
47
const YAML_BACKGROUND_DARK: syntect::highlighting::Color = syntect::highlighting::Color {
48
  r: 35,
49
  g: 50,
50
  b: 55,
51
  a: 255,
52
}; // corresponds to TEAL
53

54
fn get_syntax_set() -> &'static syntect::parsing::SyntaxSet {
×
55
  static SYNTAX_SET: OnceLock<syntect::parsing::SyntaxSet> = OnceLock::new();
56
  SYNTAX_SET.get_or_init(syntect::parsing::SyntaxSet::load_defaults_newlines)
×
57
}
×
58

59
fn get_yaml_syntax_reference() -> &'static syntect::parsing::SyntaxReference {
×
60
  static YAML_SYNTAX_REFERENCE: OnceLock<syntect::parsing::SyntaxReference> = OnceLock::new();
61
  YAML_SYNTAX_REFERENCE.get_or_init(|| {
×
62
    get_syntax_set()
×
63
      .find_syntax_by_extension("yaml")
64
      .unwrap()
65
      .clone()
66
  })
×
67
}
×
68

69
struct YamlThemes {
70
  dark: syntect::highlighting::Theme,
71
  light: syntect::highlighting::Theme,
72
}
73

74
fn get_yaml_themes() -> &'static YamlThemes {
×
75
  static YAML_THEMES: OnceLock<YamlThemes> = OnceLock::new();
76
  YAML_THEMES.get_or_init(|| {
×
77
    let ts = syntect::highlighting::ThemeSet::load_defaults();
×
78
    let mut dark = ts.themes["Solarized (dark)"].clone();
×
79
    dark.settings.background = Some(YAML_BACKGROUND_DARK);
×
80
    let mut light = ts.themes["Solarized (light)"].clone();
×
81
    light.settings.background = Some(YAML_BACKGROUND_LIGHT);
×
82
    YamlThemes { dark, light }
×
83
  })
×
84
}
×
85

86
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
1,811!
87
pub enum Styles {
88
  Default,
89
  Header,
90
  Logo,
91
  Failure,
92
  Warning,
93
  Success,
94
  Primary,
95
  Secondary,
96
  Help,
97
  Background,
98
}
99

100
pub fn theme_styles(light: bool) -> BTreeMap<Styles, Style> {
77✔
101
  if light {
77!
102
    BTreeMap::from([
×
103
      (Styles::Default, Style::default().fg(COLOR_GRAY)),
×
104
      (Styles::Header, Style::default().fg(COLOR_DARK_GRAY)),
×
105
      (Styles::Logo, Style::default().fg(COLOR_GREEN_DARK)),
×
106
      (Styles::Failure, Style::default().fg(COLOR_RED_DARK)),
×
107
      (Styles::Warning, Style::default().fg(COLOR_ORANGE_DARK)),
×
108
      (Styles::Success, Style::default().fg(COLOR_GREEN_DARK)),
×
109
      (Styles::Primary, Style::default().fg(COLOR_BLUE)),
×
110
      (Styles::Secondary, Style::default().fg(COLOR_MAGENTA_DARK)),
×
111
      (Styles::Help, Style::default().fg(COLOR_BLUE)),
×
112
      (
×
113
        Styles::Background,
×
114
        Style::default().bg(COLOR_WHITE).fg(COLOR_GRAY),
×
115
      ),
116
    ])
117
  } else {
118
    BTreeMap::from([
77✔
119
      (Styles::Default, Style::default().fg(COLOR_WHITE)),
77✔
120
      (Styles::Header, Style::default().fg(COLOR_DARK_GRAY)),
77✔
121
      (Styles::Logo, Style::default().fg(COLOR_GREEN)),
77✔
122
      (Styles::Failure, Style::default().fg(COLOR_RED)),
77✔
123
      (Styles::Warning, Style::default().fg(COLOR_ORANGE)),
77✔
124
      (Styles::Success, Style::default().fg(COLOR_GREEN)),
77✔
125
      (Styles::Primary, Style::default().fg(COLOR_CYAN)),
77✔
126
      (Styles::Secondary, Style::default().fg(COLOR_YELLOW)),
77✔
127
      (Styles::Help, Style::default().fg(COLOR_LIGHT_BLUE)),
77✔
128
      (
77✔
129
        Styles::Background,
77✔
130
        Style::default().bg(COLOR_TEAL).fg(COLOR_WHITE),
77✔
131
      ),
132
    ])
133
  }
134
}
77✔
135

136
pub fn title_style(txt: &str) -> Span<'_> {
1✔
137
  Span::styled(txt, style_bold())
1✔
138
}
1✔
139

140
pub fn style_header_text(light: bool) -> Style {
×
141
  *theme_styles(light).get(&Styles::Header).unwrap()
×
142
}
×
143

144
pub fn style_header() -> Style {
×
145
  Style::default().bg(COLOR_MAGENTA)
×
146
}
×
147

148
pub fn style_bold() -> Style {
1✔
149
  Style::default().add_modifier(Modifier::BOLD)
1✔
150
}
1✔
151

152
pub fn style_default(light: bool) -> Style {
20✔
153
  *theme_styles(light).get(&Styles::Default).unwrap()
20✔
154
}
20✔
155
pub fn style_logo(light: bool) -> Style {
×
156
  *theme_styles(light).get(&Styles::Logo).unwrap()
×
157
}
×
158
pub fn style_failure(light: bool) -> Style {
1✔
159
  *theme_styles(light).get(&Styles::Failure).unwrap()
1✔
160
}
1✔
161
pub fn style_warning(light: bool) -> Style {
×
162
  *theme_styles(light).get(&Styles::Warning).unwrap()
×
163
}
×
164
pub fn style_success(light: bool) -> Style {
×
165
  *theme_styles(light).get(&Styles::Success).unwrap()
×
166
}
×
167
pub fn style_primary(light: bool) -> Style {
47✔
168
  *theme_styles(light).get(&Styles::Primary).unwrap()
47✔
169
}
47✔
170
pub fn style_help(light: bool) -> Style {
×
171
  *theme_styles(light).get(&Styles::Help).unwrap()
×
172
}
×
173

174
pub fn style_secondary(light: bool) -> Style {
9✔
175
  *theme_styles(light).get(&Styles::Secondary).unwrap()
9✔
176
}
9✔
177

178
pub fn style_main_background(light: bool) -> Style {
×
179
  *theme_styles(light).get(&Styles::Background).unwrap()
×
180
}
×
181

182
pub fn style_highlight() -> Style {
5✔
183
  Style::default().add_modifier(Modifier::REVERSED)
5✔
184
}
5✔
185

186
pub fn get_gauge_style(enhanced_graphics: bool) -> symbols::line::Set {
×
187
  if enhanced_graphics {
×
188
    symbols::line::THICK
×
189
  } else {
190
    symbols::line::NORMAL
×
191
  }
192
}
×
193

194
pub fn table_header_style(cells: Vec<&str>, light: bool) -> Row<'_> {
4✔
195
  Row::new(cells).style(style_default(light)).bottom_margin(0)
4✔
196
}
4✔
197

198
pub fn horizontal_chunks(constraints: Vec<Constraint>, size: Rect) -> Rc<[Rect]> {
×
199
  Layout::default()
×
200
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
×
201
      &constraints,
202
    ))
203
    .direction(Direction::Horizontal)
×
204
    .split(size)
205
}
×
206

207
pub fn horizontal_chunks_with_margin(
×
208
  constraints: Vec<Constraint>,
209
  size: Rect,
210
  margin: u16,
211
) -> Rc<[Rect]> {
212
  Layout::default()
×
213
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
×
214
      &constraints,
215
    ))
216
    .direction(Direction::Horizontal)
×
217
    .margin(margin)
218
    .split(size)
219
}
×
220

221
pub fn vertical_chunks(constraints: Vec<Constraint>, size: Rect) -> Rc<[Rect]> {
1✔
222
  Layout::default()
3✔
223
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
1✔
224
      &constraints,
225
    ))
226
    .direction(Direction::Vertical)
1✔
227
    .split(size)
228
}
1✔
229

230
pub fn vertical_chunks_with_margin(
1✔
231
  constraints: Vec<Constraint>,
232
  size: Rect,
233
  margin: u16,
234
) -> Rc<[Rect]> {
235
  Layout::default()
3✔
236
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
1✔
237
      &constraints,
238
    ))
239
    .direction(Direction::Vertical)
1✔
240
    .margin(margin)
241
    .split(size)
242
}
1✔
243

244
pub fn layout_block(title: Span<'_>) -> Block<'_> {
1✔
245
  Block::default().borders(Borders::ALL).title(title)
1✔
246
}
1✔
247

248
pub fn layout_block_default(title: &str) -> Block<'_> {
1✔
249
  layout_block(title_style(title))
1✔
250
}
1✔
251

252
pub fn layout_block_active(title: &str, light: bool) -> Block<'_> {
×
253
  layout_block(title_style(title)).style(style_secondary(light))
×
254
}
×
255

256
pub fn layout_block_active_span(title: Line<'_>, light: bool) -> Block<'_> {
1✔
257
  Block::default()
2✔
258
    .borders(Borders::ALL)
259
    .title(title)
260
    .style(style_secondary(light))
1✔
261
}
1✔
262

263
pub fn layout_block_top_border(title: Line<'_>) -> Block<'_> {
4✔
264
  Block::default().borders(Borders::TOP).title(title)
4✔
265
}
4✔
266

267
pub fn title_with_dual_style<'a>(part_1: String, part_2: String, light: bool) -> Line<'a> {
5✔
268
  Line::from(vec![
10✔
269
    Span::styled(part_1, style_secondary(light).add_modifier(Modifier::BOLD)),
5✔
270
    Span::styled(part_2, style_default(light).add_modifier(Modifier::BOLD)),
5!
271
  ])
272
}
5✔
273

274
/// helper function to create a centered rect using up
275
/// certain percentage of the available rect `r`
276
pub fn centered_rect(width: u16, height: u16, r: Rect) -> Rect {
×
277
  let Rect {
278
    width: grid_width,
×
279
    height: grid_height,
×
280
    ..
281
  } = r;
282
  let outer_height = (grid_height / 2).saturating_sub(height / 2);
×
283

284
  let popup_layout = Layout::default()
×
285
    .direction(Direction::Vertical)
×
286
    .constraints(
287
      [
×
288
        Constraint::Length(outer_height),
×
289
        Constraint::Length(height),
×
290
        Constraint::Length(outer_height),
×
291
      ]
292
      .as_ref(),
293
    )
294
    .split(r);
×
295

296
  let outer_width = (grid_width / 2).saturating_sub(width / 2);
×
297

298
  Layout::default()
×
299
    .direction(Direction::Horizontal)
×
300
    .constraints(
301
      [
×
302
        Constraint::Length(outer_width),
×
303
        Constraint::Length(width),
×
304
        Constraint::Length(outer_width),
×
305
      ]
306
      .as_ref(),
307
    )
308
    .split(popup_layout[1])[1]
×
309
}
×
310

311
pub fn loading(f: &mut Frame<'_>, block: Block<'_>, area: Rect, is_loading: bool, light: bool) {
×
312
  if is_loading {
×
313
    let text = "\n\n Loading ...\n\n".to_owned();
×
314
    let text = Text::from(text);
×
315
    let text = text.patch_style(style_secondary(light));
×
316

317
    // Contains the text
318
    let paragraph = Paragraph::new(text)
×
319
      .style(style_secondary(light))
×
320
      .block(block);
321
    f.render_widget(paragraph, area);
×
322
  } else {
323
    f.render_widget(block, area)
×
324
  }
325
}
×
326

327
// using a macro to reuse code as generics will make handling lifetimes a PITA
328
#[macro_export]
329
macro_rules! draw_resource_tab {
330
  ($title:expr, $block:expr, $f:expr, $app:expr, $area:expr, $fn1:expr, $fn2:expr, $res:expr) => {
331
    match $block {
332
      ActiveBlock::Describe => draw_describe_block(
333
        $f,
334
        $app,
335
        $area,
336
        title_with_dual_style(
337
          get_resource_title($app, $title, get_describe_active($block), $res.items.len()),
338
          format!("{} | {} <esc> ", COPY_HINT, $title),
339
          $app.light_theme,
340
        ),
341
      ),
342
      ActiveBlock::Yaml => draw_yaml_block(
343
        $f,
344
        $app,
345
        $area,
346
        title_with_dual_style(
347
          get_resource_title($app, $title, get_describe_active($block), $res.items.len()),
348
          format!("{} | {} <esc> ", COPY_HINT, $title),
349
          $app.light_theme,
350
        ),
351
      ),
352
      ActiveBlock::Namespaces => $fn1($app.get_prev_route().active_block, $f, $app, $area),
353
      _ => $fn2($f, $app, $area),
354
    };
355
  };
356
}
357

358
pub struct ResourceTableProps<'a, T> {
359
  pub title: String,
360
  pub inline_help: String,
361
  pub resource: &'a mut StatefulTable<T>,
362
  pub table_headers: Vec<&'a str>,
363
  pub column_widths: Vec<Constraint>,
364
}
365
/// common for all resources
366
pub fn draw_describe_block(f: &mut Frame<'_>, app: &App, area: Rect, title: Line<'_>) {
×
367
  draw_yaml_block(f, app, area, title);
×
368
}
×
369

370
/// common for all resources
371
pub fn draw_yaml_block(f: &mut Frame<'_>, app: &App, area: Rect, title: Line<'_>) {
×
372
  let block = layout_block_top_border(title);
×
373

374
  let txt = &app.data.describe_out.get_txt();
×
375
  if !txt.is_empty() {
×
376
    let ss = get_syntax_set();
×
377
    let syntax = get_yaml_syntax_reference();
×
378
    let theme = if app.light_theme {
×
379
      &get_yaml_themes().light
×
380
    } else {
381
      &get_yaml_themes().dark
×
382
    };
383
    let mut h = syntect::easy::HighlightLines::new(syntax, theme);
×
384
    let lines: Vec<_> = syntect::util::LinesWithEndings::from(txt)
×
385
      .filter_map(|line| {
×
386
        match h.highlight_line(line, ss) {
×
387
          Ok(segments) => {
×
388
            let line_spans: Vec<_> = segments
×
389
              .into_iter()
390
              .filter_map(|segment| syntect_tui::into_span(segment).ok())
×
391
              .collect();
392
            Some(ratatui::text::Line::from(
×
393
              line_spans.into_iter().collect::<Vec<_>>(),
×
394
            ))
395
          }
396
          Err(_) => None, // Handle the error gracefully
×
397
        }
398
      })
×
399
      .collect();
400

401
    let paragraph = Paragraph::new(lines)
×
402
      .block(block)
403
      .wrap(Wrap { trim: false })
404
      .scroll((app.data.describe_out.offset, 0));
×
405
    f.render_widget(paragraph, area);
×
406
  } else {
×
407
    loading(f, block, area, app.is_loading, app.light_theme);
×
408
  }
409
}
×
410

411
/// Draw a kubernetes resource overview tab
412
pub fn draw_resource_block<'a, T: KubeResource<U>, F, U: Serialize>(
4✔
413
  f: &mut Frame<'_>,
414
  area: Rect,
415
  table_props: ResourceTableProps<'a, T>,
416
  row_cell_mapper: F,
417
  light_theme: bool,
418
  is_loading: bool,
419
  filter: Option<String>,
420
) where
421
  F: Fn(&T) -> Row<'a>,
422
{
423
  let title = title_with_dual_style(table_props.title, table_props.inline_help, light_theme);
4✔
424
  let block = layout_block_top_border(title);
4✔
425

426
  if !table_props.resource.items.is_empty() {
4!
427
    let rows = table_props.resource.items.iter().filter_map(|c| {
14✔
428
      let mapper = row_cell_mapper(c);
10✔
429
      // return only rows that match filter if filter is set
430
      match filter.as_ref() {
10!
431
        None => Some(mapper),
4✔
432
        Some(ft) if filter_by_name(ft, c) => Some(mapper),
6!
433
        _ => None,
2✔
434
      }
435
    });
10!
436

437
    let table = Table::new(rows, &table_props.column_widths)
16✔
438
      .header(table_header_style(table_props.table_headers, light_theme))
4✔
439
      .block(block)
440
      .highlight_style(style_highlight())
4✔
441
      .highlight_symbol(HIGHLIGHT);
4✔
442

443
    f.render_stateful_widget(table, area, &mut table_props.resource.state);
4✔
444
  } else {
445
    loading(f, block, area, is_loading, light_theme);
×
446
  }
447
}
4!
448

449
pub fn filter_by_resource_name<T: KubeResource<U>, U: Serialize>(
×
450
  filter: Option<String>,
451
  res: &T,
452
  row_cell_mapper: Row<'static>,
453
) -> Option<Row<'static>> {
454
  match filter.as_ref() {
×
455
    None => Some(row_cell_mapper),
×
456
    Some(ft) if filter_by_name(ft, res) => Some(row_cell_mapper),
×
457
    _ => None,
×
458
  }
459
}
×
460

461
fn filter_by_name<T: KubeResource<U>, U: Serialize>(ft: &str, res: &T) -> bool {
6✔
462
  ft.is_empty()
8!
463
    || glob_match(&ft.to_lowercase(), &res.get_name().to_lowercase())
6!
464
    || res.get_name().to_lowercase().contains(&ft.to_lowercase())
4✔
465
}
6✔
466

467
pub fn get_cluster_wide_resource_title<S: AsRef<str>>(
2✔
468
  title: S,
469
  items_len: usize,
470
  suffix: S,
471
) -> String {
472
  format!(" {} [{}] {}", title.as_ref(), items_len, suffix.as_ref())
2✔
473
}
2✔
474

475
pub fn get_resource_title<S: AsRef<str>>(
3✔
476
  app: &App,
477
  title: S,
478
  suffix: S,
479
  items_len: usize,
480
) -> String {
481
  format!(
6✔
482
    " {} {}",
483
    title_with_ns(
3✔
484
      title.as_ref(),
3✔
485
      app
6✔
486
        .data
487
        .selected
488
        .ns
489
        .as_ref()
490
        .unwrap_or(&String::from("all")),
3✔
491
      items_len
492
    ),
493
    suffix.as_ref(),
3✔
494
  )
495
}
3✔
496

497
static DESCRIBE_ACTIVE: &str = "-> Describe ";
498
static YAML_ACTIVE: &str = "-> YAML ";
499

500
pub fn get_describe_active<'a>(block: ActiveBlock) -> &'a str {
×
501
  match block {
×
502
    ActiveBlock::Describe => DESCRIBE_ACTIVE,
×
503
    _ => YAML_ACTIVE,
×
504
  }
505
}
×
506

507
pub fn title_with_ns(title: &str, ns: &str, length: usize) -> String {
4✔
508
  format!("{} (ns: {}) [{}]", title, ns, length)
4✔
509
}
4✔
510

511
#[cfg(test)]
512
mod tests {
513
  use ratatui::{backend::TestBackend, buffer::Buffer, style::Modifier, widgets::Cell, Terminal};
514

515
  use super::*;
516
  use crate::ui::utils::{COLOR_CYAN, COLOR_WHITE, COLOR_YELLOW};
517

518
  #[test]
519
  fn test_draw_resource_block() {
2✔
520
    let backend = TestBackend::new(100, 6);
1✔
521
    let mut terminal = Terminal::new(backend).unwrap();
1✔
522

523
    struct RenderTest {
524
      pub name: String,
525
      pub namespace: String,
526
      pub data: i32,
527
      pub age: String,
528
    }
529

530
    impl KubeResource<Option<String>> for RenderTest {
531
      fn get_name(&self) -> &String {
×
532
        &self.name
533
      }
×
534
      fn get_k8s_obj(&self) -> &Option<String> {
×
535
        &None
536
      }
×
537
    }
538
    terminal
1✔
539
      .draw(|f| {
1✔
540
        let size = f.size();
1✔
541
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
542
        resource.set_items(vec![
2✔
543
          RenderTest {
1✔
544
            name: "Test 1".into(),
1✔
545
            namespace: "Test ns".into(),
1✔
546
            age: "65h3m".into(),
1✔
547
            data: 5,
548
          },
549
          RenderTest {
1✔
550
            name: "Test long name that should be truncated from view".into(),
1✔
551
            namespace: "Test ns".into(),
1✔
552
            age: "65h3m".into(),
1✔
553
            data: 3,
554
          },
555
          RenderTest {
1✔
556
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
557
            namespace: "Test ns long value check that should be truncated".into(),
1✔
558
            age: "65h3m".into(),
1!
559
            data: 6,
560
          },
561
        ]);
562
        draw_resource_block(
1✔
563
          f,
564
          size,
565
          ResourceTableProps {
1✔
566
            title: "Test".into(),
1✔
567
            inline_help: "-> yaml <y>".into(),
1✔
568
            resource: &mut resource,
569
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1!
570
            column_widths: vec![
2✔
571
              Constraint::Percentage(30),
1✔
572
              Constraint::Percentage(40),
1✔
573
              Constraint::Percentage(15),
1✔
574
              Constraint::Percentage(15),
1!
575
            ],
576
          },
577
          |c| {
3✔
578
            Row::new(vec![
6✔
579
              Cell::from(c.namespace.to_owned()),
3✔
580
              Cell::from(c.name.to_owned()),
3✔
581
              Cell::from(c.data.to_string()),
3✔
582
              Cell::from(c.age.to_owned()),
3!
583
            ])
584
            .style(style_primary(false))
3✔
585
          },
3✔
586
          false,
587
          false,
588
          None,
1✔
589
        );
590
      })
1✔
591
      .unwrap();
592

593
    let mut expected = Buffer::with_lines(vec![
1!
594
        "Test-> yaml <y>─────────────────────────────────────────────────────────────────────────────────────",
595
        "   Namespace                     Name                                 Data           Age            ",
596
        "=> Test ns                       Test 1                               5              65h3m          ",
597
        "   Test ns                       Test long name that should be trunca 3              65h3m          ",
598
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
599
        "                                                                                                    ",
600
      ]);
601
    // set row styles
602
    // First row heading style
603
    for col in 0..=99 {
101✔
604
      match col {
605
        0..=3 => {
100!
606
          expected.get_mut(col, 0).set_style(
8✔
607
            Style::default()
4✔
608
              .fg(COLOR_YELLOW)
609
              .add_modifier(Modifier::BOLD),
610
          );
611
        }
612
        4..=14 => {
96!
613
          expected.get_mut(col, 0).set_style(
22✔
614
            Style::default()
11✔
615
              .fg(COLOR_WHITE)
616
              .add_modifier(Modifier::BOLD),
617
          );
618
        }
619
        _ => {}
620
      }
621
    }
622

623
    // Second row table header style
624
    for col in 0..=99 {
101✔
625
      expected
200✔
626
        .get_mut(col, 1)
627
        .set_style(Style::default().fg(COLOR_WHITE));
100✔
628
    }
629
    // first table data row style
630
    for col in 0..=99 {
101✔
631
      expected.get_mut(col, 2).set_style(
200✔
632
        Style::default()
100✔
633
          .fg(COLOR_CYAN)
634
          .add_modifier(Modifier::REVERSED),
635
      );
636
    }
637
    // remaining table data row style
638
    for row in 3..=4 {
3✔
639
      for col in 0..=99 {
202✔
640
        expected
400✔
641
          .get_mut(col, row)
642
          .set_style(Style::default().fg(COLOR_CYAN));
200✔
643
      }
644
    }
645

646
    terminal.backend().assert_buffer(&expected);
1✔
647
  }
2✔
648

649
  #[test]
650
  fn test_draw_resource_block_filter() {
2✔
651
    let backend = TestBackend::new(100, 6);
1✔
652
    let mut terminal = Terminal::new(backend).unwrap();
1✔
653

654
    struct RenderTest {
655
      pub name: String,
656
      pub namespace: String,
657
      pub data: i32,
658
      pub age: String,
659
    }
660
    impl KubeResource<Option<String>> for RenderTest {
661
      fn get_name(&self) -> &String {
6✔
662
        &self.name
663
      }
6✔
664
      fn get_k8s_obj(&self) -> &Option<String> {
×
665
        &None
666
      }
×
667
    }
668

669
    terminal
1✔
670
      .draw(|f| {
1✔
671
        let size = f.size();
1✔
672
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
673
        resource.set_items(vec![
2✔
674
          RenderTest {
1✔
675
            name: "Test 1".into(),
1✔
676
            namespace: "Test ns".into(),
1✔
677
            age: "65h3m".into(),
1✔
678
            data: 5,
679
          },
680
          RenderTest {
1✔
681
            name: "Test long name that should be truncated from view".into(),
1✔
682
            namespace: "Test ns".into(),
1✔
683
            age: "65h3m".into(),
1✔
684
            data: 3,
685
          },
686
          RenderTest {
1✔
687
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
688
            namespace: "Test ns long value check that should be truncated".into(),
1✔
689
            age: "65h3m".into(),
1!
690
            data: 6,
691
          },
692
        ]);
693
        draw_resource_block(
1✔
694
          f,
695
          size,
696
          ResourceTableProps {
1✔
697
            title: "Test".into(),
1✔
698
            inline_help: "-> yaml <y>".into(),
1✔
699
            resource: &mut resource,
700
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1!
701
            column_widths: vec![
2✔
702
              Constraint::Percentage(30),
1✔
703
              Constraint::Percentage(40),
1✔
704
              Constraint::Percentage(15),
1✔
705
              Constraint::Percentage(15),
1!
706
            ],
707
          },
708
          |c| {
3✔
709
            Row::new(vec![
6✔
710
              Cell::from(c.namespace.to_owned()),
3✔
711
              Cell::from(c.name.to_owned()),
3✔
712
              Cell::from(c.data.to_string()),
3✔
713
              Cell::from(c.age.to_owned()),
3!
714
            ])
715
            .style(style_primary(false))
3✔
716
          },
3✔
717
          false,
718
          false,
719
          Some("truncated".to_string()),
1✔
720
        );
721
      })
1✔
722
      .unwrap();
723

724
    let mut expected = Buffer::with_lines(vec![
1!
725
        "Test-> yaml <y>─────────────────────────────────────────────────────────────────────────────────────",
726
        "   Namespace                     Name                                 Data           Age            ",
727
        "=> Test ns                       Test long name that should be trunca 3              65h3m          ",
728
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
729
        "                                                                                                    ",
730
        "                                                                                                    ",
731
      ]);
732
    // set row styles
733
    // First row heading style
734
    for col in 0..=99 {
101✔
735
      match col {
736
        0..=3 => {
100!
737
          expected.get_mut(col, 0).set_style(
8✔
738
            Style::default()
4✔
739
              .fg(COLOR_YELLOW)
740
              .add_modifier(Modifier::BOLD),
741
          );
742
        }
743
        4..=14 => {
96!
744
          expected.get_mut(col, 0).set_style(
22✔
745
            Style::default()
11✔
746
              .fg(COLOR_WHITE)
747
              .add_modifier(Modifier::BOLD),
748
          );
749
        }
750
        _ => {}
751
      }
752
    }
753

754
    // Second row table header style
755
    for col in 0..=99 {
101✔
756
      expected
200✔
757
        .get_mut(col, 1)
758
        .set_style(Style::default().fg(COLOR_WHITE));
100✔
759
    }
760
    // first table data row style
761
    for col in 0..=99 {
101✔
762
      expected.get_mut(col, 2).set_style(
200✔
763
        Style::default()
100✔
764
          .fg(COLOR_CYAN)
765
          .add_modifier(Modifier::REVERSED),
766
      );
767
    }
768
    // remaining table data row style
769
    for row in 3..=3 {
2✔
770
      for col in 0..=99 {
101✔
771
        expected
200✔
772
          .get_mut(col, row)
773
          .set_style(Style::default().fg(COLOR_CYAN));
100✔
774
      }
775
    }
776

777
    terminal.backend().assert_buffer(&expected);
1✔
778
  }
2✔
779

780
  #[test]
781
  fn test_draw_resource_block_filter_glob() {
2✔
782
    let backend = TestBackend::new(100, 6);
1✔
783
    let mut terminal = Terminal::new(backend).unwrap();
1✔
784

785
    struct RenderTest {
786
      pub name: String,
787
      pub namespace: String,
788
      pub data: i32,
789
      pub age: String,
790
    }
791
    impl KubeResource<Option<String>> for RenderTest {
792
      fn get_name(&self) -> &String {
4✔
793
        &self.name
794
      }
4✔
795
      fn get_k8s_obj(&self) -> &Option<String> {
×
796
        &None
797
      }
×
798
    }
799

800
    terminal
1✔
801
      .draw(|f| {
1✔
802
        let size = f.size();
1✔
803
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
804
        resource.set_items(vec![
2✔
805
          RenderTest {
1✔
806
            name: "Test 1".into(),
1✔
807
            namespace: "Test ns".into(),
1✔
808
            age: "65h3m".into(),
1✔
809
            data: 5,
810
          },
811
          RenderTest {
1✔
812
            name: "Test long name that should be truncated from view".into(),
1✔
813
            namespace: "Test ns".into(),
1✔
814
            age: "65h3m".into(),
1✔
815
            data: 3,
816
          },
817
          RenderTest {
1✔
818
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
819
            namespace: "Test ns long value check that should be truncated".into(),
1✔
820
            age: "65h3m".into(),
1!
821
            data: 6,
822
          },
823
        ]);
824
        draw_resource_block(
1✔
825
          f,
826
          size,
827
          ResourceTableProps {
1✔
828
            title: "Test".into(),
1✔
829
            inline_help: "-> yaml <y>".into(),
1✔
830
            resource: &mut resource,
831
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1!
832
            column_widths: vec![
2✔
833
              Constraint::Percentage(30),
1✔
834
              Constraint::Percentage(40),
1✔
835
              Constraint::Percentage(15),
1✔
836
              Constraint::Percentage(15),
1!
837
            ],
838
          },
839
          |c| {
3✔
840
            Row::new(vec![
6✔
841
              Cell::from(c.namespace.to_owned()),
3✔
842
              Cell::from(c.name.to_owned()),
3✔
843
              Cell::from(c.data.to_string()),
3✔
844
              Cell::from(c.age.to_owned()),
3!
845
            ])
846
            .style(style_primary(false))
3✔
847
          },
3✔
848
          false,
849
          false,
850
          Some("*long*truncated*".to_string()),
1✔
851
        );
852
      })
1✔
853
      .unwrap();
854

855
    let mut expected = Buffer::with_lines(vec![
1!
856
        "Test-> yaml <y>─────────────────────────────────────────────────────────────────────────────────────",
857
        "   Namespace                     Name                                 Data           Age            ",
858
        "=> Test ns                       Test long name that should be trunca 3              65h3m          ",
859
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
860
        "                                                                                                    ",
861
        "                                                                                                    ",
862
      ]);
863
    // set row styles
864
    // First row heading style
865
    for col in 0..=99 {
101✔
866
      match col {
867
        0..=3 => {
100!
868
          expected.get_mut(col, 0).set_style(
8✔
869
            Style::default()
4✔
870
              .fg(COLOR_YELLOW)
871
              .add_modifier(Modifier::BOLD),
872
          );
873
        }
874
        4..=14 => {
96!
875
          expected.get_mut(col, 0).set_style(
22✔
876
            Style::default()
11✔
877
              .fg(COLOR_WHITE)
878
              .add_modifier(Modifier::BOLD),
879
          );
880
        }
881
        _ => {}
882
      }
883
    }
884

885
    // Second row table header style
886
    for col in 0..=99 {
101✔
887
      expected
200✔
888
        .get_mut(col, 1)
889
        .set_style(Style::default().fg(COLOR_WHITE));
100✔
890
    }
891
    // first table data row style
892
    for col in 0..=99 {
101✔
893
      expected.get_mut(col, 2).set_style(
200✔
894
        Style::default()
100✔
895
          .fg(COLOR_CYAN)
896
          .add_modifier(Modifier::REVERSED),
897
      );
898
    }
899
    // remaining table data row style
900
    for row in 3..=3 {
2✔
901
      for col in 0..=99 {
101✔
902
        expected
200✔
903
          .get_mut(col, row)
904
          .set_style(Style::default().fg(COLOR_CYAN));
100✔
905
      }
906
    }
907

908
    terminal.backend().assert_buffer(&expected);
1✔
909
  }
2✔
910

911
  #[test]
912
  fn test_get_resource_title() {
2✔
913
    let app = App::default();
1✔
914
    assert_eq!(
1✔
915
      get_resource_title(&app, "Title", "-> hello", 5),
1!
916
      " Title (ns: all) [5] -> hello"
917
    );
918
  }
2✔
919

920
  #[test]
921
  fn test_title_with_ns() {
2✔
922
    assert_eq!(title_with_ns("Title", "hello", 3), "Title (ns: hello) [3]");
1!
923
  }
2✔
924

925
  #[test]
926
  fn test_get_cluster_wide_resource_title() {
2✔
927
    assert_eq!(
1✔
928
      get_cluster_wide_resource_title("Cluster Resource", 3, ""),
1!
929
      " Cluster Resource [3] "
930
    );
931
    assert_eq!(
1✔
932
      get_cluster_wide_resource_title("Nodes", 10, "-> hello"),
1!
933
      " Nodes [10] -> hello"
934
    );
935
  }
2✔
936
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc