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

kdash-rs / kdash / 3750513782

pending completion
3750513782

push

github

Deepu
fix tests

982 of 4441 branches covered (22.11%)

Branch coverage included in aggregate %.

2731 of 4724 relevant lines covered (57.81%)

8.14 hits per line

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

50.83
/src/app/mod.rs
1
pub(crate) mod configmaps;
2
pub(crate) mod contexts;
3
pub(crate) mod cronjobs;
4
pub(crate) mod daemonsets;
5
pub(crate) mod deployments;
6
pub(crate) mod jobs;
7
pub(crate) mod key_binding;
8
pub(crate) mod metrics;
9
pub(crate) mod models;
10
pub(crate) mod nodes;
11
pub(crate) mod ns;
12
pub(crate) mod pods;
13
pub(crate) mod replicasets;
14
pub(crate) mod replication_controllers;
15
pub(crate) mod roles;
16
pub(crate) mod secrets;
17
pub(crate) mod statefulsets;
18
pub(crate) mod storageclass;
19
pub(crate) mod svcs;
20
mod utils;
21

22
use anyhow::anyhow;
23
use kube::config::Kubeconfig;
24
use kubectl_view_allocations::{GroupBy, QtyByQualifier};
25
use tokio::sync::mpsc::Sender;
26
use tui::layout::Rect;
27

28
use self::{
29
  configmaps::KubeConfigMap,
30
  contexts::KubeContext,
31
  cronjobs::KubeCronJob,
32
  daemonsets::KubeDaemonSet,
33
  deployments::KubeDeployment,
34
  jobs::KubeJob,
35
  key_binding::DEFAULT_KEYBINDING,
36
  metrics::KubeNodeMetrics,
37
  models::{LogsState, ScrollableTxt, StatefulList, StatefulTable, TabRoute, TabsState},
38
  nodes::KubeNode,
39
  ns::KubeNs,
40
  pods::{KubeContainer, KubePod},
41
  replicasets::KubeReplicaSet,
42
  replication_controllers::KubeReplicationController,
43
  roles::{KubeClusterRole, KubeClusterRoleBinding, KubeRole, KubeRoleBinding},
44
  secrets::KubeSecret,
45
  statefulsets::KubeStatefulSet,
46
  storageclass::KubeStorageClass,
47
  svcs::KubeSvc,
48
};
49
use super::{
50
  cmd::IoCmdEvent,
51
  network::{stream::IoStreamEvent, IoEvent},
52
};
53

54
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
15!
55
pub enum ActiveBlock {
56
  Help,
57
  Pods,
58
  Containers,
59
  Logs,
60
  Services,
61
  Nodes,
62
  Deployments,
63
  ConfigMaps,
64
  StatefulSets,
65
  ReplicaSets,
66
  Namespaces,
67
  Describe,
68
  Yaml,
69
  Contexts,
70
  Utilization,
71
  Jobs,
72
  DaemonSets,
73
  CronJobs,
74
  Secrets,
75
  RplCtrl,
76
  StorageClasses,
77
  Roles,
78
  RoleBindings,
79
  ClusterRoles,
80
  ClusterRoleBinding,
81
  More,
82
}
83

84
#[derive(Clone, Eq, PartialEq, Debug)]
4!
85
pub enum RouteId {
86
  Home,
87
  Contexts,
88
  Utilization,
89
  HelpMenu,
90
}
91

92
#[derive(Debug, Clone)]
8✔
93
pub struct Route {
94
  pub id: RouteId,
4✔
95
  pub active_block: ActiveBlock,
4✔
96
}
97

98
const DEFAULT_ROUTE: Route = Route {
99
  id: RouteId::Home,
100
  active_block: ActiveBlock::Pods,
101
};
102

103
/// Holds CLI version info
104
pub struct Cli {
105
  pub name: String,
106
  pub version: String,
107
  pub status: bool,
108
}
109

110
/// Holds data state for various views
111
pub struct Data {
112
  pub selected: Selected,
113
  pub clis: Vec<Cli>,
114
  pub kubeconfig: Option<Kubeconfig>,
115
  pub contexts: StatefulTable<KubeContext>,
116
  pub active_context: Option<KubeContext>,
117
  pub node_metrics: Vec<KubeNodeMetrics>,
118
  pub logs: LogsState,
119
  pub describe_out: ScrollableTxt,
120
  pub metrics: StatefulTable<(Vec<String>, Option<QtyByQualifier>)>,
121
  pub namespaces: StatefulTable<KubeNs>,
122
  pub nodes: StatefulTable<KubeNode>,
123
  pub pods: StatefulTable<KubePod>,
124
  pub containers: StatefulTable<KubeContainer>,
125
  pub services: StatefulTable<KubeSvc>,
126
  pub config_maps: StatefulTable<KubeConfigMap>,
127
  pub stateful_sets: StatefulTable<KubeStatefulSet>,
128
  pub replica_sets: StatefulTable<KubeReplicaSet>,
129
  pub deployments: StatefulTable<KubeDeployment>,
130
  pub jobs: StatefulTable<KubeJob>,
131
  pub daemon_sets: StatefulTable<KubeDaemonSet>,
132
  pub cronjobs: StatefulTable<KubeCronJob>,
133
  pub secrets: StatefulTable<KubeSecret>,
134
  pub rpl_ctrls: StatefulTable<KubeReplicationController>,
135
  pub storage_classes: StatefulTable<KubeStorageClass>,
136
  pub roles: StatefulTable<KubeRole>,
137
  pub role_bindings: StatefulTable<KubeRoleBinding>,
138
  pub cluster_roles: StatefulTable<KubeClusterRole>,
139
  pub cluster_role_binding: StatefulTable<KubeClusterRoleBinding>,
140
}
141

142
/// selected data items
143
pub struct Selected {
144
  pub ns: Option<String>,
145
  pub pod: Option<String>,
146
  pub container: Option<String>,
147
  pub context: Option<String>,
148
}
149

150
/// Holds main application state
151
pub struct App {
152
  navigation_stack: Vec<Route>,
153
  io_tx: Option<Sender<IoEvent>>,
154
  io_stream_tx: Option<Sender<IoStreamEvent>>,
155
  io_cmd_tx: Option<Sender<IoCmdEvent>>,
156
  pub title: &'static str,
157
  pub should_quit: bool,
158
  pub main_tabs: TabsState,
159
  pub context_tabs: TabsState,
160
  pub more_resources_menu: StatefulList<(String, ActiveBlock)>,
161
  pub show_info_bar: bool,
162
  pub is_loading: bool,
163
  pub is_streaming: bool,
164
  pub is_routing: bool,
165
  pub tick_until_poll: u64,
166
  pub tick_count: u64,
167
  pub enhanced_graphics: bool,
168
  pub table_cols: u16,
169
  pub size: Rect,
170
  pub api_error: String,
171
  pub dialog: Option<String>,
172
  pub confirm: bool,
173
  pub light_theme: bool,
174
  pub refresh: bool,
175
  pub log_auto_scroll: bool,
176
  pub utilization_group_by: Vec<GroupBy>,
177
  pub help_docs: StatefulTable<Vec<String>>,
178
  pub data: Data,
179
}
180

181
impl Default for Data {
182
  fn default() -> Self {
14✔
183
    Data {
14✔
184
      clis: vec![],
14✔
185
      kubeconfig: None,
14✔
186
      contexts: StatefulTable::new(),
14✔
187
      active_context: None,
14✔
188
      node_metrics: vec![],
14✔
189
      namespaces: StatefulTable::new(),
14✔
190
      selected: Selected {
14✔
191
        ns: None,
14✔
192
        pod: None,
14✔
193
        container: None,
14✔
194
        context: None,
14✔
195
      },
196
      logs: LogsState::new(String::default()),
14✔
197
      describe_out: ScrollableTxt::new(),
14✔
198
      metrics: StatefulTable::new(),
14✔
199
      nodes: StatefulTable::new(),
14✔
200
      pods: StatefulTable::new(),
14✔
201
      containers: StatefulTable::new(),
14✔
202
      services: StatefulTable::new(),
14✔
203
      config_maps: StatefulTable::new(),
14✔
204
      stateful_sets: StatefulTable::new(),
14✔
205
      replica_sets: StatefulTable::new(),
14✔
206
      deployments: StatefulTable::new(),
14✔
207
      jobs: StatefulTable::new(),
14✔
208
      daemon_sets: StatefulTable::new(),
14✔
209
      cronjobs: StatefulTable::new(),
14✔
210
      secrets: StatefulTable::new(),
14✔
211
      rpl_ctrls: StatefulTable::new(),
14✔
212
      storage_classes: StatefulTable::new(),
14✔
213
      roles: StatefulTable::new(),
14✔
214
      role_bindings: StatefulTable::new(),
14✔
215
      cluster_roles: StatefulTable::new(),
14✔
216
      cluster_role_binding: StatefulTable::new(),
14✔
217
    }
218
  }
14✔
219
}
220

221
impl Default for App {
222
  fn default() -> Self {
14✔
223
    App {
14✔
224
      navigation_stack: vec![DEFAULT_ROUTE],
14✔
225
      io_tx: None,
14✔
226
      io_stream_tx: None,
14✔
227
      io_cmd_tx: None,
14✔
228
      title: " KDash - A simple Kubernetes dashboard ",
229
      should_quit: false,
230
      main_tabs: TabsState::new(vec![
28✔
231
        TabRoute {
14✔
232
          title: format!(
14✔
233
            "Active Context {}",
234
            DEFAULT_KEYBINDING.jump_to_current_context.key
235
          ),
236
          route: Route {
14✔
237
            active_block: ActiveBlock::Pods,
238
            id: RouteId::Home,
239
          },
240
        },
241
        TabRoute {
14✔
242
          title: format!(
14✔
243
            "All Contexts {}",
244
            DEFAULT_KEYBINDING.jump_to_all_context.key
245
          ),
246
          route: Route {
14✔
247
            active_block: ActiveBlock::Contexts,
248
            id: RouteId::Contexts,
249
          },
250
        },
251
        TabRoute {
14✔
252
          title: format!("Utilization {}", DEFAULT_KEYBINDING.jump_to_utilization.key),
14✔
253
          route: Route {
14✔
254
            active_block: ActiveBlock::Utilization,
255
            id: RouteId::Utilization,
256
          },
257
        },
258
      ]),
259
      context_tabs: TabsState::new(vec![
28✔
260
        TabRoute {
14✔
261
          title: format!("Pods {}", DEFAULT_KEYBINDING.jump_to_pods.key),
14✔
262
          route: Route {
14✔
263
            active_block: ActiveBlock::Pods,
264
            id: RouteId::Home,
265
          },
266
        },
267
        TabRoute {
14✔
268
          title: format!("Services {}", DEFAULT_KEYBINDING.jump_to_services.key),
14✔
269
          route: Route {
14✔
270
            active_block: ActiveBlock::Services,
271
            id: RouteId::Home,
272
          },
273
        },
274
        TabRoute {
14✔
275
          title: format!("Nodes {}", DEFAULT_KEYBINDING.jump_to_nodes.key),
14✔
276
          route: Route {
14✔
277
            active_block: ActiveBlock::Nodes,
278
            id: RouteId::Home,
279
          },
280
        },
281
        TabRoute {
14✔
282
          title: format!("ConfigMaps {}", DEFAULT_KEYBINDING.jump_to_configmaps.key),
14✔
283
          route: Route {
14✔
284
            active_block: ActiveBlock::ConfigMaps,
285
            id: RouteId::Home,
286
          },
287
        },
288
        TabRoute {
14✔
289
          title: format!(
14✔
290
            "StatefulSets {}",
291
            DEFAULT_KEYBINDING.jump_to_statefulsets.key
292
          ),
293
          route: Route {
14✔
294
            active_block: ActiveBlock::StatefulSets,
295
            id: RouteId::Home,
296
          },
297
        },
298
        TabRoute {
14✔
299
          title: format!("ReplicaSets {}", DEFAULT_KEYBINDING.jump_to_replicasets.key),
14✔
300
          route: Route {
14✔
301
            active_block: ActiveBlock::ReplicaSets,
302
            id: RouteId::Home,
303
          },
304
        },
305
        TabRoute {
14✔
306
          title: format!("Deployments {}", DEFAULT_KEYBINDING.jump_to_deployments.key),
14✔
307
          route: Route {
14✔
308
            active_block: ActiveBlock::Deployments,
309
            id: RouteId::Home,
310
          },
311
        },
312
        TabRoute {
14✔
313
          title: format!("Jobs {}", DEFAULT_KEYBINDING.jump_to_jobs.key),
14✔
314
          route: Route {
14✔
315
            active_block: ActiveBlock::Jobs,
316
            id: RouteId::Home,
317
          },
318
        },
319
        TabRoute {
14✔
320
          title: format!("DaemonSets {}", DEFAULT_KEYBINDING.jump_to_daemonsets.key),
14✔
321
          route: Route {
14✔
322
            active_block: ActiveBlock::DaemonSets,
323
            id: RouteId::Home,
324
          },
325
        },
326
        TabRoute {
14✔
327
          title: format!("More {}", DEFAULT_KEYBINDING.jump_to_more_resources.key),
14✔
328
          route: Route {
14✔
329
            active_block: ActiveBlock::More,
330
            id: RouteId::Home,
331
          },
332
        },
333
      ]),
334
      more_resources_menu: StatefulList::with_items(vec![
28✔
335
        ("Cron Jobs".into(), ActiveBlock::CronJobs),
14✔
336
        ("Secrets".into(), ActiveBlock::Secrets),
14✔
337
        ("Replication Controllers".into(), ActiveBlock::RplCtrl),
14✔
338
        // ("Persistent Volume Claims".into(), ActiveBlock::RplCtrl),
339
        // ("Persistent Volumes".into(), ActiveBlock::RplCtrl),
340
        ("Storage Classes".into(), ActiveBlock::StorageClasses),
14✔
341
        ("Roles".into(), ActiveBlock::Roles),
14✔
342
        ("Role Bindings".into(), ActiveBlock::RoleBindings),
14✔
343
        ("Cluster Roles".into(), ActiveBlock::ClusterRoles),
14✔
344
        (
14✔
345
          "Cluster Role Bindings".into(),
14✔
346
          ActiveBlock::ClusterRoleBinding,
347
        ),
348
        // ("Service Accounts".into(), ActiveBlock::RplCtrl),
349
        // ("Ingresses".into(), ActiveBlock::RplCtrl),
350
        // ("Network Policies".into(), ActiveBlock::RplCtrl),
351
      ]),
352
      show_info_bar: true,
353
      is_loading: false,
354
      is_streaming: false,
355
      is_routing: false,
356
      tick_until_poll: 0,
357
      tick_count: 0,
358
      enhanced_graphics: false,
359
      table_cols: 0,
360
      size: Rect::default(),
14✔
361
      api_error: String::new(),
14✔
362
      dialog: None,
14✔
363
      confirm: false,
364
      light_theme: false,
365
      refresh: true,
366
      log_auto_scroll: true,
367
      utilization_group_by: vec![
28✔
368
        GroupBy::resource,
14✔
369
        GroupBy::node,
14✔
370
        GroupBy::namespace,
14✔
371
        GroupBy::pod,
14✔
372
      ],
373
      help_docs: StatefulTable::with_items(key_binding::get_help_docs()),
14✔
374
      data: Data::default(),
14✔
375
    }
376
  }
14✔
377
}
378

379
impl App {
380
  pub fn new(
×
381
    io_tx: Sender<IoEvent>,
382
    io_stream_tx: Sender<IoStreamEvent>,
383
    io_cmd_tx: Sender<IoCmdEvent>,
384
    enhanced_graphics: bool,
385
    tick_until_poll: u64,
386
  ) -> Self {
387
    App {
×
388
      io_tx: Some(io_tx),
×
389
      io_stream_tx: Some(io_stream_tx),
×
390
      io_cmd_tx: Some(io_cmd_tx),
×
391
      enhanced_graphics,
392
      tick_until_poll,
393
      ..App::default()
×
394
    }
395
  }
×
396

397
  pub fn reset(&mut self) {
×
398
    self.tick_count = 0;
×
399
    self.api_error = String::new();
×
400
    self.data = Data::default();
×
401
    self.route_home();
×
402
  }
×
403

404
  // Send a network event to the network thread
405
  pub async fn dispatch(&mut self, action: IoEvent) {
94!
406
    // `is_loading` will be set to false again after the async action has finished in network/mod.rs
407
    self.is_loading = true;
47✔
408
    if let Some(io_tx) = &self.io_tx {
94!
409
      if let Err(e) = io_tx.send(action).await {
47!
410
        self.is_loading = false;
×
411
        self.handle_error(anyhow!(e));
×
412
      };
413
    }
414
  }
94✔
415

416
  // Send a stream event to the stream network thread
417
  pub async fn dispatch_stream(&mut self, action: IoStreamEvent) {
4!
418
    // `is_loading` will be set to false again after the async action has finished in network/stream.rs
419
    self.is_loading = true;
2✔
420
    if let Some(io_stream_tx) = &self.io_stream_tx {
4!
421
      if let Err(e) = io_stream_tx.send(action).await {
2!
422
        self.is_loading = false;
×
423
        self.handle_error(anyhow!(e));
×
424
      };
425
    }
426
  }
4✔
427

428
  // Send a cmd event to the cmd runner thread
429
  pub async fn dispatch_cmd(&mut self, action: IoCmdEvent) {
12!
430
    // `is_loading` will be set to false again after the async action has finished in network/stream.rs
431
    self.is_loading = true;
4✔
432
    if let Some(io_cmd_tx) = &self.io_cmd_tx {
6✔
433
      if let Err(e) = io_cmd_tx.send(action).await {
2!
434
        self.is_loading = false;
×
435
        self.handle_error(anyhow!(e));
×
436
      };
2!
437
    }
438
  }
8✔
439

440
  pub fn set_contexts(&mut self, contexts: Vec<KubeContext>) {
×
441
    self.data.active_context = contexts.iter().find_map(|ctx| {
×
442
      if ctx.is_active {
×
443
        Some(ctx.clone())
×
444
      } else {
445
        None
×
446
      }
447
    });
×
448
    self.data.contexts.set_items(contexts);
×
449
  }
×
450

451
  pub fn handle_error(&mut self, e: anyhow::Error) {
×
452
    self.api_error = e.to_string();
×
453
  }
×
454

455
  pub fn push_navigation_stack(&mut self, id: RouteId, active_block: ActiveBlock) {
5✔
456
    self.push_navigation_route(Route { id, active_block });
5✔
457
  }
5✔
458

459
  pub fn push_navigation_route(&mut self, route: Route) {
9✔
460
    self.navigation_stack.push(route);
9✔
461
    self.is_routing = true;
9✔
462
  }
9✔
463

464
  pub fn pop_navigation_stack(&mut self) -> Option<Route> {
×
465
    self.is_routing = true;
×
466
    if self.navigation_stack.len() == 1 {
×
467
      None
×
468
    } else {
469
      self.navigation_stack.pop()
×
470
    }
471
  }
×
472

473
  pub fn get_current_route(&self) -> &Route {
14✔
474
    // if for some reason there is no route return the default
475
    self.navigation_stack.last().unwrap_or(&DEFAULT_ROUTE)
14✔
476
  }
14✔
477

478
  pub fn get_prev_route(&self) -> &Route {
×
479
    // get the previous route
480
    self.get_nth_route_from_last(1)
×
481
  }
×
482

483
  pub fn get_nth_route_from_last(&self, index: usize) -> &Route {
×
484
    // get the previous route by index
485
    let index = self.navigation_stack.len().saturating_sub(index + 1);
×
486
    if index > 0 {
×
487
      &self.navigation_stack[index]
×
488
    } else {
489
      &self.navigation_stack[0]
×
490
    }
491
  }
×
492

493
  pub fn cycle_main_routes(&mut self) {
×
494
    self.main_tabs.next();
×
495
    let route = self.main_tabs.get_active_route().clone();
×
496
    self.push_navigation_route(route);
×
497
  }
×
498

499
  pub fn route_home(&mut self) {
3✔
500
    let route = self.main_tabs.set_index(0).route.clone();
3✔
501
    self.push_navigation_route(route);
3✔
502
  }
3✔
503

504
  pub fn route_contexts(&mut self) {
1✔
505
    let route = self.main_tabs.set_index(1).route.clone();
1✔
506
    self.push_navigation_route(route);
1✔
507
  }
1✔
508

509
  pub fn route_utilization(&mut self) {
×
510
    let route = self.main_tabs.set_index(2).route.clone();
×
511
    self.push_navigation_route(route);
×
512
  }
×
513

514
  pub async fn dispatch_container_logs(&mut self, id: String) {
×
515
    self.data.logs = LogsState::new(id);
×
516
    self.push_navigation_stack(RouteId::Home, ActiveBlock::Logs);
×
517
    self.dispatch_stream(IoStreamEvent::GetPodLogs(true)).await;
×
518
  }
×
519

520
  pub fn refresh(&mut self) {
1✔
521
    self.refresh = true;
1✔
522
  }
1✔
523

524
  pub async fn cache_all_resource_data(&mut self) {
4!
525
    self.dispatch(IoEvent::GetNamespaces).await;
2!
526
    self.dispatch(IoEvent::GetPods).await;
2!
527
    self.dispatch(IoEvent::GetServices).await;
2!
528
    self.dispatch(IoEvent::GetConfigMaps).await;
2!
529
    self.dispatch(IoEvent::GetStatefulSets).await;
2!
530
    self.dispatch(IoEvent::GetReplicaSets).await;
2!
531
    self.dispatch(IoEvent::GetDeployments).await;
2!
532
    self.dispatch(IoEvent::GetJobs).await;
2!
533
    self.dispatch(IoEvent::GetDaemonSets).await;
2!
534
    self.dispatch(IoEvent::GetCronJobs).await;
2!
535
    self.dispatch(IoEvent::GetSecrets).await;
2!
536
    self.dispatch(IoEvent::GetReplicationControllers).await;
2!
537
    self.dispatch(IoEvent::GetStorageClasses).await;
2!
538
    self.dispatch(IoEvent::GetRoles).await;
2!
539
    self.dispatch(IoEvent::GetRoleBindings).await;
2!
540
    self.dispatch(IoEvent::GetClusterRoles).await;
2!
541
    self.dispatch(IoEvent::GetClusterRoleBinding).await;
2!
542
    self.dispatch(IoEvent::GetMetrics).await;
2!
543
  }
4✔
544

545
  pub async fn dispatch_by_active_block(&mut self, active_block: ActiveBlock) {
6!
546
    match active_block {
3!
547
      ActiveBlock::Pods | ActiveBlock::Containers => {
2✔
548
        self.dispatch(IoEvent::GetPods).await;
2!
549
      }
550
      ActiveBlock::Services => {
×
551
        self.dispatch(IoEvent::GetServices).await;
×
552
      }
553
      ActiveBlock::ConfigMaps => {
×
554
        self.dispatch(IoEvent::GetConfigMaps).await;
×
555
      }
556
      ActiveBlock::StatefulSets => {
×
557
        self.dispatch(IoEvent::GetStatefulSets).await;
×
558
      }
559
      ActiveBlock::ReplicaSets => {
×
560
        self.dispatch(IoEvent::GetReplicaSets).await;
×
561
      }
562
      ActiveBlock::Deployments => {
×
563
        self.dispatch(IoEvent::GetDeployments).await;
×
564
      }
565
      ActiveBlock::Jobs => {
×
566
        self.dispatch(IoEvent::GetJobs).await;
×
567
      }
568
      ActiveBlock::DaemonSets => {
×
569
        self.dispatch(IoEvent::GetDaemonSets).await;
×
570
      }
571
      ActiveBlock::CronJobs => {
×
572
        self.dispatch(IoEvent::GetCronJobs).await;
×
573
      }
574
      ActiveBlock::Secrets => {
×
575
        self.dispatch(IoEvent::GetSecrets).await;
×
576
      }
577
      ActiveBlock::RplCtrl => {
×
578
        self.dispatch(IoEvent::GetReplicationControllers).await;
×
579
      }
580
      ActiveBlock::StorageClasses => {
×
581
        self.dispatch(IoEvent::GetStorageClasses).await;
×
582
      }
583
      ActiveBlock::Roles => {
×
584
        self.dispatch(IoEvent::GetRoles).await;
×
585
      }
586
      ActiveBlock::RoleBindings => {
×
587
        self.dispatch(IoEvent::GetRoleBindings).await;
×
588
      }
589
      ActiveBlock::ClusterRoles => {
×
590
        self.dispatch(IoEvent::GetClusterRoles).await;
×
591
      }
592
      ActiveBlock::ClusterRoleBinding => {
×
593
        self.dispatch(IoEvent::GetClusterRoleBinding).await;
×
594
      }
595
      ActiveBlock::Logs => {
596
        if !self.is_streaming {
2!
597
          // do not tail to avoid duplicates
598
          self.dispatch_stream(IoStreamEvent::GetPodLogs(false)).await;
1!
599
        }
600
      }
601
      _ => {}
×
602
    }
603
  }
6✔
604

605
  pub async fn on_tick(&mut self, first_render: bool) {
6!
606
    // Make one time requests on first render or refresh
607
    if self.refresh {
5✔
608
      if !first_render {
3✔
609
        self.dispatch(IoEvent::RefreshClient).await;
1!
610
        self.dispatch_stream(IoStreamEvent::RefreshClient).await;
1!
611
      }
612
      self.dispatch(IoEvent::GetKubeConfig).await;
2!
613
      // call these once to pre-load data
614
      self.cache_all_resource_data().await;
2!
615
      self.refresh = false;
2✔
616
    }
617
    // make network requests only in intervals to avoid hogging up the network
618
    if self.tick_count % self.tick_until_poll == 0 || self.is_routing {
6!
619
      // make periodic network calls based on active route and active block to avoid hogging
620
      match self.get_current_route().id {
3!
621
        RouteId::Home => {
622
          if self.data.clis.is_empty() {
6!
623
            self.dispatch_cmd(IoCmdEvent::GetCliInfo).await;
3!
624
          }
625
          self.dispatch(IoEvent::GetNamespaces).await;
3!
626
          self.dispatch(IoEvent::GetNodes).await;
3!
627

628
          let active_block = self.get_current_route().active_block;
3✔
629
          if active_block == ActiveBlock::Namespaces {
6!
630
            self
×
631
              .dispatch_by_active_block(self.get_prev_route().active_block)
×
632
              .await;
×
633
          } else {
634
            self.dispatch_by_active_block(active_block).await;
3!
635
          }
636
        }
637
        RouteId::Utilization => {
×
638
          self.dispatch(IoEvent::GetMetrics).await;
×
639
        }
640
        _ => {}
×
641
      }
642
      self.is_routing = false;
3✔
643
    }
644

645
    self.tick_count += 1;
3✔
646
  }
6✔
647
}
648

649
/// utility methods for tests
650
#[cfg(test)]
651
#[macro_use]
652
mod test_utils {
653
  use std::{fmt, fs};
654

655
  use k8s_openapi::{
656
    apimachinery::pkg::apis::meta::v1::Time,
657
    chrono::{DateTime, TimeZone, Utc},
658
  };
659
  use kube::{api::ObjectList, Resource};
660
  use serde::{de::DeserializeOwned, Serialize};
661

662
  use super::models::KubeResource;
663

664
  pub fn convert_resource_from_file<K: Serialize, T>(filename: &str) -> (Vec<T>, Vec<K>)
17✔
665
  where
666
    <K as Resource>::DynamicType: Default,
667
    K: Clone + DeserializeOwned + fmt::Debug,
668
    K: Resource,
669
    T: KubeResource<K> + From<K>,
670
  {
671
    let res_list = load_resource_from_file(filename);
17✔
672
    let original_res_list = res_list.items.clone();
17✔
673

674
    let resources: Vec<T> = res_list.into_iter().map(K::into).collect::<Vec<_>>();
17✔
675

676
    (resources, original_res_list)
17✔
677
  }
17✔
678

679
  pub fn load_resource_from_file<K>(filename: &str) -> ObjectList<K>
20✔
680
  where
681
    <K as Resource>::DynamicType: Default,
682
    K: Clone + DeserializeOwned + fmt::Debug,
683
    K: Resource,
684
  {
685
    let yaml = fs::read_to_string(format!("./test_data/{}.yaml", filename))
20✔
686
      .expect("Something went wrong reading yaml file");
687
    assert_ne!(yaml, "".to_string());
20!
688

689
    let res_list: serde_yaml::Result<ObjectList<K>> = serde_yaml::from_str(&yaml);
20✔
690
    assert!(res_list.is_ok(), "{:?}", res_list.err());
20!
691
    res_list.unwrap()
20✔
692
  }
20✔
693

694
  pub fn get_time(s: &str) -> Time {
50✔
695
    Time(to_utc(s))
50✔
696
  }
50✔
697

698
  fn to_utc(s: &str) -> DateTime<Utc> {
50✔
699
    Utc.datetime_from_str(s, "%Y-%m-%dT%H:%M:%SZ").unwrap()
50✔
700
  }
50✔
701

702
  #[macro_export]
703
  macro_rules! map_string_object {
704
    // map-like
705
    ($($k:expr => $v:expr),* $(,)?) => {
706
        std::iter::Iterator::collect(IntoIterator::into_iter([$(($k.to_string(), $v),)*]))
707
    };
708
  }
709
}
710

711
#[cfg(test)]
712
mod tests {
713
  use tokio::sync::mpsc;
714

715
  use super::*;
716

717
  #[tokio::test]
718
  async fn test_on_tick_first_render() {
3!
719
    let (sync_io_tx, mut sync_io_rx) = mpsc::channel::<IoEvent>(500);
1✔
720
    let (sync_io_cmd_tx, mut sync_io_cmd_rx) = mpsc::channel::<IoCmdEvent>(500);
1✔
721

722
    let mut app = App {
1✔
723
      tick_until_poll: 2,
724
      io_tx: Some(sync_io_tx),
1✔
725
      io_cmd_tx: Some(sync_io_cmd_tx),
1✔
726
      ..App::default()
1✔
727
    };
1✔
728

729
    assert_eq!(app.tick_count, 0);
1!
730
    // test first render
731
    app.on_tick(true).await;
1!
732
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetKubeConfig);
1!
733
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetNamespaces);
1!
734
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetPods);
1!
735
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetServices);
1!
736
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetConfigMaps);
1!
737
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetStatefulSets);
1!
738
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetReplicaSets);
1!
739
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetDeployments);
1!
740
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetJobs);
1!
741
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetDaemonSets);
1!
742
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetCronJobs);
1!
743
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetSecrets);
1!
744
    assert_eq!(
1✔
745
      sync_io_rx.recv().await.unwrap(),
1!
746
      IoEvent::GetReplicationControllers
747
    );
748
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetStorageClasses);
1!
749
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetRoles);
1!
750
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetRoleBindings);
1!
751
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetClusterRoles);
1!
752
    assert_eq!(
1✔
753
      sync_io_rx.recv().await.unwrap(),
1!
754
      IoEvent::GetClusterRoleBinding
755
    );
756
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetMetrics);
1!
757
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetNamespaces);
1!
758
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetNodes);
1!
759
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetPods);
1!
760

761
    assert_eq!(sync_io_cmd_rx.recv().await.unwrap(), IoCmdEvent::GetCliInfo);
1!
762

763
    assert!(!app.refresh);
1!
764
    assert!(!app.is_routing);
1!
765
    assert_eq!(app.tick_count, 1);
2!
766
  }
3✔
767
  #[tokio::test]
768
  async fn test_on_tick_refresh_tick_limit() {
3!
769
    let (sync_io_tx, mut sync_io_rx) = mpsc::channel::<IoEvent>(500);
1✔
770
    let (sync_io_stream_tx, mut sync_io_stream_rx) = mpsc::channel::<IoStreamEvent>(500);
1✔
771
    let (sync_io_cmd_tx, mut sync_io_cmd_rx) = mpsc::channel::<IoCmdEvent>(500);
1✔
772

773
    let mut app = App {
1✔
774
      tick_until_poll: 2,
775
      tick_count: 2,
776
      refresh: true,
777
      io_tx: Some(sync_io_tx),
1✔
778
      io_stream_tx: Some(sync_io_stream_tx),
1✔
779
      io_cmd_tx: Some(sync_io_cmd_tx),
1✔
780
      ..App::default()
1✔
781
    };
1✔
782

783
    assert_eq!(app.tick_count, 2);
1!
784
    // test first render
785
    app.on_tick(false).await;
1!
786
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::RefreshClient);
1!
787
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetKubeConfig);
1!
788
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetNamespaces);
1!
789
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetPods);
1!
790
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetServices);
1!
791
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetConfigMaps);
1!
792
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetStatefulSets);
1!
793
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetReplicaSets);
1!
794
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetDeployments);
1!
795

796
    assert_eq!(
1✔
797
      sync_io_stream_rx.recv().await.unwrap(),
1!
798
      IoStreamEvent::RefreshClient
799
    );
800
    assert_eq!(sync_io_cmd_rx.recv().await.unwrap(), IoCmdEvent::GetCliInfo);
1!
801

802
    assert!(!app.refresh);
1!
803
    assert!(!app.is_routing);
1!
804
    assert_eq!(app.tick_count, 3);
2!
805
  }
3✔
806
  #[tokio::test]
807
  async fn test_on_tick_routing() {
3!
808
    let (sync_io_tx, mut sync_io_rx) = mpsc::channel::<IoEvent>(500);
1✔
809
    let (sync_io_stream_tx, mut sync_io_stream_rx) = mpsc::channel::<IoStreamEvent>(500);
1✔
810

811
    let mut app = App {
1✔
812
      tick_until_poll: 2,
813
      tick_count: 2,
814
      is_routing: true,
815
      refresh: false,
816
      io_tx: Some(sync_io_tx),
1✔
817
      io_stream_tx: Some(sync_io_stream_tx),
1✔
818
      ..App::default()
1✔
819
    };
1✔
820

821
    app.push_navigation_stack(RouteId::Home, ActiveBlock::Logs);
1✔
822

823
    assert_eq!(app.tick_count, 2);
1!
824
    // test first render
825
    app.on_tick(false).await;
1!
826
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetNamespaces);
1!
827
    assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetNodes);
1!
828

829
    assert_eq!(
1✔
830
      sync_io_stream_rx.recv().await.unwrap(),
1!
831
      IoStreamEvent::GetPodLogs(false)
832
    );
833

834
    assert!(!app.refresh);
1!
835
    assert!(!app.is_routing);
1!
836
    assert_eq!(app.tick_count, 3);
2!
837
  }
3✔
838
}
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