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

kdash-rs / kdash / 24138980530

08 Apr 2026 01:50PM UTC coverage: 65.099% (-0.07%) from 65.167%
24138980530

push

github

web-flow
Merge pull request #504 from sed-i/feature/events

feat: add "Events" under the "More" menu

99 of 176 new or added lines in 5 files covered. (56.25%)

1 existing line in 1 file now uncovered.

8125 of 12481 relevant lines covered (65.1%)

137.96 hits per line

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

38.01
/src/ui/resource_tabs.rs
1
use ratatui::{
2
  layout::{Constraint, Rect},
3
  text::Line,
4
  widgets::{List, ListItem, ListState, Tabs},
5
  Frame,
6
};
7

8
use super::{
9
  utils::{
10
    centered_rect, default_part, filter_bar_title, filter_cursor_position, help_part,
11
    layout_block_default, layout_block_default_line, layout_block_top_border, mixed_bold_line,
12
    mixed_line, split_hint_suffix, style_highlight, style_secondary, vertical_chunks,
13
    vertical_chunks_with_margin,
14
  },
15
  HIGHLIGHT,
16
};
17
use crate::app::{
18
  configmaps::ConfigMapResource,
19
  cronjobs::CronJobResource,
20
  daemonsets::DaemonSetResource,
21
  deployments::DeploymentResource,
22
  dynamic::DynamicResource,
23
  events::EventResource,
24
  ingress::IngressResource,
25
  jobs::JobResource,
26
  key_binding::DEFAULT_KEYBINDING,
27
  models::{AppResource, StatefulList},
28
  network_policies::NetworkPolicyResource,
29
  nodes::NodeResource,
30
  pods::PodResource,
31
  pvcs::PvcResource,
32
  pvs::PvResource,
33
  replicasets::ReplicaSetResource,
34
  replication_controllers::ReplicationControllerResource,
35
  roles::{ClusterRoleBindingResource, ClusterRoleResource, RoleBindingResource, RoleResource},
36
  secrets::SecretResource,
37
  serviceaccounts::SvcAcctResource,
38
  statefulsets::StatefulSetResource,
39
  storageclass::StorageClassResource,
40
  svcs::SvcResource,
41
  ActiveBlock, App,
42
};
43

44
pub fn draw_resource_tabs_block(f: &mut Frame<'_>, app: &mut App, area: Rect) {
1✔
45
  let current_filter = app
1✔
46
    .current_or_selected_resource_table()
1✔
47
    .map(|table| (table.filter_text(), table.is_filter_active()));
1✔
48
  let chunks = vertical_chunks_with_margin(
1✔
49
    vec![
1✔
50
      Constraint::Length(2),
1✔
51
      Constraint::Length(2),
1✔
52
      Constraint::Min(0),
1✔
53
    ],
54
    area,
1✔
55
    1,
56
  );
57

58
  let mut block = layout_block_default(" Resources ");
1✔
59
  if app.get_current_route().active_block != ActiveBlock::Namespaces {
1✔
60
    block = block.style(style_secondary(app.light_theme))
1✔
61
  }
×
62

63
  let titles: Vec<Line<'_>> = app
1✔
64
    .context_tabs
1✔
65
    .items
1✔
66
    .iter()
1✔
67
    .enumerate()
1✔
68
    .map(|(i, t)| {
11✔
69
      let count = tab_count_label(app, i);
11✔
70
      let (name, hint) = split_hint_suffix(&t.title);
11✔
71
      if i == app.context_tabs.index {
11✔
72
        Line::from(format!("{} [{}]", name, count))
1✔
73
      } else if let Some(hint) = hint {
10✔
74
        mixed_line(
10✔
75
          [
10✔
76
            default_part(format!("{} [{}]", name, count)),
10✔
77
            help_part(format!(" {}", hint)),
10✔
78
          ],
10✔
79
          app.light_theme,
10✔
80
        )
81
      } else {
82
        Line::from(format!("{} [{}]", t.title, count))
×
83
      }
84
    })
11✔
85
    .collect();
1✔
86
  let tabs = Tabs::new(titles)
1✔
87
    .block(block)
1✔
88
    .highlight_style(style_secondary(app.light_theme))
1✔
89
    .select(app.context_tabs.index);
1✔
90

91
  f.render_widget(tabs, area);
1✔
92
  let filter_chunks = vertical_chunks(
1✔
93
    vec![Constraint::Length(1), Constraint::Length(1)],
1✔
94
    chunks[1],
1✔
95
  );
96
  draw_filter_bar(f, app, filter_chunks[0], current_filter);
1✔
97
  let content_chunk = chunks[2];
1✔
98

99
  // render tab content
100
  match app.context_tabs.index {
1✔
101
    0 => PodResource::render(app.get_current_route().active_block, f, app, content_chunk),
1✔
102
    1 => SvcResource::render(app.get_current_route().active_block, f, app, content_chunk),
×
103
    2 => NodeResource::render(app.get_current_route().active_block, f, app, content_chunk),
×
104
    3 => ConfigMapResource::render(app.get_current_route().active_block, f, app, content_chunk),
×
105
    4 => StatefulSetResource::render(app.get_current_route().active_block, f, app, content_chunk),
×
106
    5 => ReplicaSetResource::render(app.get_current_route().active_block, f, app, content_chunk),
×
107
    6 => DeploymentResource::render(app.get_current_route().active_block, f, app, content_chunk),
×
108
    7 => JobResource::render(app.get_current_route().active_block, f, app, content_chunk),
×
109
    8 => DaemonSetResource::render(app.get_current_route().active_block, f, app, content_chunk),
×
110
    9 | 10 => draw_more(app.get_current_route().active_block, f, app, content_chunk),
×
111
    _ => {}
×
112
  };
113
}
1✔
114

115
fn tab_count_label(app: &App, index: usize) -> String {
11✔
116
  app
11✔
117
    .context_tab_resource_table(index)
11✔
118
    .map_or_else(|| "0".to_string(), |table| table.count_label())
11✔
119
}
11✔
120

121
fn draw_filter_bar(f: &mut Frame<'_>, app: &App, area: Rect, current_filter: Option<(&str, bool)>) {
1✔
122
  let (filter, filter_active) = current_filter.unwrap_or(("", false));
1✔
123
  let title = filter_bar_title(filter, filter_active, app.light_theme);
1✔
124

125
  f.render_widget(layout_block_top_border(title), area);
1✔
126

127
  if filter_active {
1✔
128
    f.set_cursor_position(filter_cursor_position(area, 1, filter));
×
129
  }
1✔
130
}
1✔
131

132
/// more resources tab
133
fn draw_more(block: ActiveBlock, f: &mut Frame<'_>, app: &mut App, area: Rect) {
×
134
  // Collect counts before borrowing menu mutably
135
  let counts: Vec<(ActiveBlock, usize)> = vec![
×
136
    (ActiveBlock::CronJobs, app.data.cronjobs.items.len()),
×
137
    (ActiveBlock::Secrets, app.data.secrets.items.len()),
×
138
    (
×
139
      ActiveBlock::ReplicationControllers,
×
140
      app.data.replication_controllers.items.len(),
×
141
    ),
×
142
    (
×
143
      ActiveBlock::StorageClasses,
×
144
      app.data.storage_classes.items.len(),
×
145
    ),
×
146
    (ActiveBlock::Roles, app.data.roles.items.len()),
×
147
    (
×
148
      ActiveBlock::RoleBindings,
×
149
      app.data.role_bindings.items.len(),
×
150
    ),
×
151
    (
×
152
      ActiveBlock::ClusterRoles,
×
153
      app.data.cluster_roles.items.len(),
×
154
    ),
×
155
    (
×
156
      ActiveBlock::ClusterRoleBindings,
×
157
      app.data.cluster_role_bindings.items.len(),
×
158
    ),
×
159
    (ActiveBlock::Ingresses, app.data.ingress.items.len()),
×
160
    (
×
161
      ActiveBlock::PersistentVolumeClaims,
×
162
      app.data.persistent_volume_claims.items.len(),
×
163
    ),
×
164
    (
×
165
      ActiveBlock::PersistentVolumes,
×
166
      app.data.persistent_volumes.items.len(),
×
167
    ),
×
168
    (
×
169
      ActiveBlock::ServiceAccounts,
×
170
      app.data.service_accounts.items.len(),
×
171
    ),
×
NEW
172
    (ActiveBlock::Events, app.data.events.items.len()),
×
173
    (
×
174
      ActiveBlock::NetworkPolicies,
×
175
      app.data.network_policies.items.len(),
×
176
    ),
×
177
  ];
178
  match block {
×
179
    ActiveBlock::More => draw_menu(
×
180
      f,
×
181
      &mut app.more_resources_menu,
×
182
      &app.menu_filter,
×
183
      app.menu_filter_active,
×
184
      &counts,
×
185
      app.light_theme,
×
186
      area,
×
187
    ),
188
    ActiveBlock::DynamicView => draw_menu(
×
189
      f,
×
190
      &mut app.dynamic_resources_menu,
×
191
      &app.menu_filter,
×
192
      app.menu_filter_active,
×
193
      &counts,
×
194
      app.light_theme,
×
195
      area,
×
196
    ),
197
    ActiveBlock::CronJobs => CronJobResource::render(block, f, app, area),
×
198
    ActiveBlock::Secrets => SecretResource::render(block, f, app, area),
×
199
    ActiveBlock::ReplicationControllers => {
200
      ReplicationControllerResource::render(block, f, app, area)
×
201
    }
202
    ActiveBlock::StorageClasses => StorageClassResource::render(block, f, app, area),
×
203
    ActiveBlock::Roles => RoleResource::render(block, f, app, area),
×
204
    ActiveBlock::RoleBindings => RoleBindingResource::render(block, f, app, area),
×
205
    ActiveBlock::ClusterRoles => ClusterRoleResource::render(block, f, app, area),
×
206
    ActiveBlock::ClusterRoleBindings => ClusterRoleBindingResource::render(block, f, app, area),
×
207
    ActiveBlock::Ingresses => IngressResource::render(block, f, app, area),
×
NEW
208
    ActiveBlock::Events => EventResource::render(block, f, app, area),
×
209
    ActiveBlock::PersistentVolumeClaims => PvcResource::render(block, f, app, area),
×
210
    ActiveBlock::PersistentVolumes => PvResource::render(block, f, app, area),
×
211
    ActiveBlock::ServiceAccounts => SvcAcctResource::render(block, f, app, area),
×
212
    ActiveBlock::NetworkPolicies => NetworkPolicyResource::render(block, f, app, area),
×
213
    ActiveBlock::DynamicResource => DynamicResource::render(block, f, app, area),
×
214
    ActiveBlock::Describe | ActiveBlock::Yaml => {
215
      let mut prev_route = app.get_prev_route();
×
216
      if prev_route.active_block == block {
×
217
        prev_route = app.get_nth_route_from_last(2);
×
218
      }
×
219
      match prev_route.active_block {
×
220
        ActiveBlock::CronJobs => CronJobResource::render(block, f, app, area),
×
221
        ActiveBlock::Secrets => SecretResource::render(block, f, app, area),
×
222
        ActiveBlock::ReplicationControllers => {
223
          ReplicationControllerResource::render(block, f, app, area)
×
224
        }
225
        ActiveBlock::StorageClasses => StorageClassResource::render(block, f, app, area),
×
226
        ActiveBlock::Roles => RoleResource::render(block, f, app, area),
×
227
        ActiveBlock::RoleBindings => RoleBindingResource::render(block, f, app, area),
×
228
        ActiveBlock::ClusterRoles => ClusterRoleResource::render(block, f, app, area),
×
229
        ActiveBlock::ClusterRoleBindings => ClusterRoleBindingResource::render(block, f, app, area),
×
230
        ActiveBlock::Ingresses => IngressResource::render(block, f, app, area),
×
NEW
231
        ActiveBlock::Events => EventResource::render(block, f, app, area),
×
232
        ActiveBlock::PersistentVolumeClaims => PvcResource::render(block, f, app, area),
×
233
        ActiveBlock::PersistentVolumes => PvResource::render(block, f, app, area),
×
234
        ActiveBlock::ServiceAccounts => SvcAcctResource::render(block, f, app, area),
×
235
        ActiveBlock::NetworkPolicies => NetworkPolicyResource::render(block, f, app, area),
×
236
        ActiveBlock::DynamicResource => DynamicResource::render(block, f, app, area),
×
237
        _ => { /* do nothing */ }
×
238
      }
239
    }
240
    ActiveBlock::Pods => crate::app::pods::draw_block_as_sub(f, app, area),
×
241
    ActiveBlock::Containers => crate::app::pods::draw_containers_block(f, app, area),
×
242
    ActiveBlock::Logs => crate::app::pods::draw_logs_block(f, app, area),
×
243
    ActiveBlock::Namespaces => draw_more(app.get_prev_route().active_block, f, app, area),
×
244
    _ => { /* do nothing */ }
×
245
  }
246
}
×
247

248
/// more resources menu
249
fn draw_menu(
×
250
  f: &mut Frame<'_>,
×
251
  more_resources_menu: &mut StatefulList<(String, ActiveBlock)>,
×
252
  filter: &str,
×
253
  filter_active: bool,
×
254
  counts: &[(ActiveBlock, usize)],
×
255
  light_theme: bool,
×
256
  area: Rect,
×
257
) {
×
258
  use crate::handlers::filter_menu_items;
259

260
  let area = centered_rect(50, 15, area);
×
261

262
  let filtered = filter_menu_items(&more_resources_menu.items, filter);
×
263
  let items: Vec<ListItem<'_>> = filtered
×
264
    .iter()
×
265
    .map(|(_, (name, block))| {
×
266
      let count = counts
×
267
        .iter()
×
268
        .find(|(b, _)| b == block)
×
269
        .map(|(_, c)| *c)
×
270
        .unwrap_or(0);
×
271
      if count > 0 {
×
272
        ListItem::new(format!("{} [{}]", name, count))
×
273
      } else {
274
        ListItem::new(name.clone())
×
275
      }
276
    })
×
277
    .collect();
×
278

279
  let title = if filter_active && !filter.is_empty() {
×
280
    mixed_bold_line(
×
281
      [
×
282
        default_part(" Select Resource ".to_string()),
×
283
        default_part(format!("[{}] ", filter)),
×
284
      ],
×
285
      light_theme,
×
286
    )
287
  } else if filter_active {
×
288
    mixed_bold_line(
×
289
      [
×
290
        default_part(" Select Resource ".to_string()),
×
291
        help_part("[type to filter] ".to_string()),
×
292
      ],
×
293
      light_theme,
×
294
    )
295
  } else {
296
    mixed_bold_line(
×
297
      [
×
298
        default_part(" Select Resource ".to_string()),
×
299
        help_part(format!("{} to filter ", DEFAULT_KEYBINDING.filter.key)),
×
300
      ],
×
301
      light_theme,
×
302
    )
303
  };
304

305
  // Use a local ListState so selection operates within filtered bounds
306
  let selected = more_resources_menu
×
307
    .state
×
308
    .selected()
×
309
    .map(|i| i.min(items.len().saturating_sub(1)));
×
310
  let mut local_state = ListState::default();
×
311
  local_state.select(selected);
×
312

313
  f.render_stateful_widget(
×
314
    List::new(items)
×
315
      .block(layout_block_default_line(title))
×
316
      .highlight_style(style_highlight())
×
317
      .highlight_symbol(HIGHLIGHT),
×
318
    area,
×
319
    &mut local_state,
×
320
  );
321

322
  if filter_active {
×
323
    f.set_cursor_position(filter_cursor_position(
×
324
      area,
×
325
      " Select Resource [".chars().count(),
×
326
      filter,
×
327
    ));
×
328
  }
×
329

330
  // Sync the clamped selection back
331
  more_resources_menu.state.select(local_state.selected());
×
332
}
×
333

334
#[cfg(test)]
335
mod tests {
336
  use ratatui::{backend::TestBackend, style::Modifier, Terminal};
337

338
  use super::*;
339
  use crate::{
340
    app::pods::KubePod,
341
    ui::utils::{MACCHIATO_BLUE, MACCHIATO_RED, MACCHIATO_TEXT, MACCHIATO_YELLOW},
342
  };
343

344
  #[test]
345
  fn test_draw_resource_tabs_block() {
1✔
346
    let backend = TestBackend::new(100, 10);
1✔
347
    let mut terminal = Terminal::new(backend).unwrap();
1✔
348

349
    terminal
1✔
350
      .draw(|f| {
1✔
351
        let size = f.area();
1✔
352
        let mut app = App::default();
1✔
353
        let mut pod = KubePod::default();
1✔
354
        pod.name = "pod name test".into();
1✔
355
        pod.namespace = "pod namespace test".into();
1✔
356
        pod.ready = (0, 2);
1✔
357
        pod.status = "Failed".into();
1✔
358
        pod.age = "6h52m".into();
1✔
359
        app.data.pods.set_items(vec![pod]);
1✔
360
        draw_resource_tabs_block(f, &mut app, size);
1✔
361
      })
1✔
362
      .unwrap();
1✔
363

364
    let buffer = terminal.backend().buffer();
1✔
365
    let lines: Vec<String> = (0..buffer.area.height)
1✔
366
      .map(|row| {
10✔
367
        (0..buffer.area.width)
10✔
368
          .map(|col| buffer[(col, row)].symbol())
1,000✔
369
          .collect::<String>()
10✔
370
      })
10✔
371
      .collect();
1✔
372
    assert_eq!(
1✔
373
      lines,
374
      vec![
1✔
375
        "┌ Resources ───────────────────────────────────────────────────────────────────────────────────────┐",
376
        "│ Pods [1] │ Services [0] <2> │ Nodes [0] <3> │ ConfigMaps [0] <4> │ StatefulSets [0] <5> │ Replica│",
1✔
377
        "│                                                                                                  │",
1✔
378
        "│ filter </> ──────────────────────────────────────────────────────────────────────────────────────│",
1✔
379
        "│                                                                                                  │",
1✔
380
        "│ Pods (ns: all) [1] Containers <Enter> | describe <d> | yaml <y> | logs <L> ──────────────────────│",
1✔
381
        "│   Namespace                Name                         Ready      Status    Restarts   Age      │",
1✔
382
        "│=> pod namespace test       pod name test                0/2        Failed    0          6h52m    │",
1✔
383
        "│                                                                                                  │",
1✔
384
        "└──────────────────────────────────────────────────────────────────────────────────────────────────┘",
1✔
385
      ]
386
    );
387

388
    assert_eq!(buffer[(0, 0)].fg, MACCHIATO_YELLOW);
1✔
389
    assert_eq!(buffer[(1, 0)].fg, MACCHIATO_YELLOW);
1✔
390
    assert!(buffer[(1, 0)].modifier.contains(Modifier::BOLD));
1✔
391
    assert_eq!(buffer[(17, 1)].fg, MACCHIATO_TEXT);
1✔
392
    assert_eq!(buffer[(1, 3)].fg, MACCHIATO_BLUE);
1✔
393
    assert_eq!(buffer[(0, 4)].fg, MACCHIATO_YELLOW);
1✔
394
    assert_eq!(buffer[(99, 4)].fg, MACCHIATO_YELLOW);
1✔
395
    assert_eq!(buffer[(1, 5)].fg, MACCHIATO_YELLOW);
1✔
396
    assert!(buffer[(1, 5)].modifier.contains(Modifier::BOLD));
1✔
397
    assert_eq!(buffer[(21, 5)].fg, MACCHIATO_BLUE);
1✔
398
    assert!(buffer[(21, 5)].modifier.contains(Modifier::BOLD));
1✔
399
    assert_eq!(buffer[(79, 5)].fg, MACCHIATO_YELLOW);
1✔
400
    assert_eq!(buffer[(1, 6)].fg, MACCHIATO_TEXT);
1✔
401
    assert_eq!(buffer[(99, 6)].fg, MACCHIATO_YELLOW);
1✔
402
    assert_eq!(buffer[(1, 7)].fg, MACCHIATO_RED);
1✔
403
    assert!(buffer[(1, 7)].modifier.contains(Modifier::REVERSED));
1✔
404
    assert_eq!(buffer[(99, 7)].fg, MACCHIATO_YELLOW);
1✔
405
  }
1✔
406
}
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

© 2026 Coveralls, Inc