• 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

62.09
/src/app/events.rs
1
use async_trait::async_trait;
2
use chrono::Utc;
3
use k8s_openapi::{
4
  api::core::v1::Event,
5
  apimachinery::pkg::apis::meta::v1::{MicroTime, Time},
6
};
7
use ratatui::{
8
  layout::{Constraint, Rect},
9
  widgets::{Cell, Row},
10
  Frame,
11
};
12

13
use super::{
14
  models::{AppResource, KubeResource},
15
  utils, ActiveBlock, App,
16
};
17
use crate::{
18
  draw_resource_tab,
19
  network::Network,
20
  ui::utils::{
21
    describe_yaml_and_esc_hint, draw_describe_block, draw_resource_block, draw_yaml_block,
22
    get_describe_active, get_resource_title, help_bold_line, style_primary, title_with_dual_style,
23
    ResourceTableProps,
24
  },
25
};
26

27
#[derive(Clone, Debug, PartialEq)]
28
pub struct KubeEvent {
29
  pub name: String,
30
  pub namespace: String,
31
  pub involved_kind: String,
32
  pub reason: String,
33
  pub message: String,
34
  pub count: i32,
35
  pub age: String,
36
  k8s_obj: Event,
37
}
38

39
impl From<Event> for KubeEvent {
40
  fn from(event: Event) -> Self {
4✔
41
    let count = event
4✔
42
      .count
4✔
43
      .or_else(|| event.series.as_ref().and_then(|series| series.count))
4✔
44
      .unwrap_or_default();
4✔
45
    let age = utils::to_age(event_timestamp(&event).as_ref(), Utc::now());
4✔
46

47
    KubeEvent {
4✔
48
      name: event.metadata.name.clone().unwrap_or_default(),
4✔
49
      namespace: event.metadata.namespace.clone().unwrap_or_default(),
4✔
50
      involved_kind: event.involved_object.kind.clone().unwrap_or_default(),
4✔
51
      reason: event.reason.clone().unwrap_or_default(),
4✔
52
      message: event.message.clone().unwrap_or_default(),
4✔
53
      count,
4✔
54
      age,
4✔
55
      k8s_obj: utils::sanitize_obj(event),
4✔
56
    }
4✔
57
  }
4✔
58
}
59

60
fn event_timestamp(event: &Event) -> Option<Time> {
4✔
61
  event
4✔
62
    .series
4✔
63
    .as_ref()
4✔
64
    .and_then(|series| series.last_observed_time.as_ref().map(micro_time_to_time))
4✔
65
    .or_else(|| event.last_timestamp.clone())
4✔
66
    .or_else(|| event.event_time.as_ref().map(micro_time_to_time))
4✔
67
    .or_else(|| event.metadata.creation_timestamp.clone())
4✔
68
}
4✔
69

70
fn micro_time_to_time(time: &MicroTime) -> Time {
3✔
71
  Time(time.0)
3✔
72
}
3✔
73

74
impl KubeResource<Event> for KubeEvent {
NEW
75
  fn get_name(&self) -> &String {
×
NEW
76
    &self.name
×
NEW
77
  }
×
NEW
78
  fn get_k8s_obj(&self) -> &Event {
×
NEW
79
    &self.k8s_obj
×
NEW
80
  }
×
81
}
82

83
static EVENTS_TITLE: &str = "Events";
84

85
pub struct EventResource {}
86

87
#[async_trait]
88
impl AppResource for EventResource {
NEW
89
  fn render(block: ActiveBlock, f: &mut Frame<'_>, app: &mut App, area: Rect) {
×
NEW
90
    draw_resource_tab!(
×
NEW
91
      EVENTS_TITLE,
×
NEW
92
      block,
×
NEW
93
      f,
×
NEW
94
      app,
×
NEW
95
      area,
×
96
      Self::render,
97
      draw_block,
98
      app.data.events
99
    );
NEW
100
  }
×
101

102
  async fn get_resource(nw: &Network<'_>) {
103
    let items: Vec<KubeEvent> = nw.get_namespaced_resources(Event::into).await;
104

105
    let mut app = nw.app.lock().await;
106
    app.data.events.set_items(items);
NEW
107
  }
×
108
}
109

NEW
110
fn draw_block(f: &mut Frame<'_>, app: &mut App, area: Rect) {
×
NEW
111
  let is_loading = app.is_loading();
×
NEW
112
  let title = get_resource_title(app, EVENTS_TITLE, "", app.data.events.items.len());
×
113

NEW
114
  draw_resource_block(
×
NEW
115
    f,
×
NEW
116
    area,
×
NEW
117
    ResourceTableProps {
×
NEW
118
      title,
×
NEW
119
      inline_help: help_bold_line(describe_yaml_and_esc_hint(), app.light_theme),
×
NEW
120
      resource: &mut app.data.events,
×
NEW
121
      table_headers: vec![
×
NEW
122
        "Namespace",
×
NEW
123
        "Name",
×
NEW
124
        "Involved Kind",
×
NEW
125
        "Reason",
×
NEW
126
        "Message",
×
NEW
127
        "Count",
×
NEW
128
        "Age",
×
NEW
129
      ],
×
NEW
130
      column_widths: vec![
×
NEW
131
        Constraint::Percentage(12),
×
NEW
132
        Constraint::Percentage(18),
×
NEW
133
        Constraint::Percentage(12),
×
NEW
134
        Constraint::Percentage(13),
×
NEW
135
        Constraint::Percentage(30),
×
NEW
136
        Constraint::Percentage(5),
×
NEW
137
        Constraint::Percentage(10),
×
NEW
138
      ],
×
NEW
139
    },
×
NEW
140
    |c| {
×
NEW
141
      Row::new(vec![
×
NEW
142
        Cell::from(c.namespace.to_owned()),
×
NEW
143
        Cell::from(c.name.to_owned()),
×
NEW
144
        Cell::from(c.involved_kind.to_owned()),
×
NEW
145
        Cell::from(c.reason.to_owned()),
×
NEW
146
        Cell::from(c.message.to_owned()),
×
NEW
147
        Cell::from(c.count.to_string()),
×
NEW
148
        Cell::from(c.age.to_owned()),
×
149
      ])
NEW
150
      .style(style_primary(app.light_theme))
×
NEW
151
    },
×
NEW
152
    app.light_theme,
×
NEW
153
    is_loading,
×
154
  );
NEW
155
}
×
156

157
#[cfg(test)]
158
mod tests {
159
  use chrono::Utc;
160
  use k8s_openapi::{
161
    api::core::v1::{EventSeries, ObjectReference},
162
    apimachinery::pkg::apis::meta::v1::{MicroTime, ObjectMeta},
163
  };
164

165
  use super::*;
166
  use crate::app::test_utils::{convert_resource_from_file, get_time};
167

168
  #[test]
169
  fn test_event_from_api() {
1✔
170
    let (events, events_list): (Vec<KubeEvent>, Vec<_>) = convert_resource_from_file("events");
1✔
171

172
    assert_eq!(events.len(), 2);
1✔
173
    assert_eq!(
1✔
174
      events[0],
1✔
175
      KubeEvent {
1✔
176
        name: "ga-edge-0.18931e4c1f3244cf".into(),
1✔
177
        namespace: "gagent".into(),
1✔
178
        involved_kind: "Pod".into(),
1✔
179
        reason: "FailedScheduling".into(),
1✔
180
        message: "0/1 nodes are available: 1 node(s) didn't match Pod's node affinity/selector. preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling."
1✔
181
          .into(),
1✔
182
        count: 3432,
1✔
183
        age: utils::to_age(Some(&get_micro_time("2026-02-23T04:41:50.537584Z")), Utc::now()),
1✔
184
        k8s_obj: utils::sanitize_obj(events_list[0].clone()),
1✔
185
      }
1✔
186
    );
187
    assert_eq!(
1✔
188
      events[1],
1✔
189
      KubeEvent {
1✔
190
        name: "ga-edge-data-8a821b67-ga-edge-0.18931e4cb66ce46b".into(),
1✔
191
        namespace: "gagent".into(),
1✔
192
        involved_kind: "PersistentVolumeClaim".into(),
1✔
193
        reason: "WaitForPodScheduled".into(),
1✔
194
        message: "waiting for pod ga-edge-0 to be scheduled".into(),
1✔
195
        count: 68646,
1✔
196
        age: utils::to_age(Some(&get_time("2026-02-23T04:46:45Z")), Utc::now()),
1✔
197
        k8s_obj: utils::sanitize_obj(events_list[1].clone()),
1✔
198
      }
1✔
199
    );
200
  }
1✔
201

202
  #[test]
203
  fn test_event_uses_series_count_when_count_missing() {
1✔
204
    let event = Event {
1✔
205
      metadata: ObjectMeta {
1✔
206
        creation_timestamp: Some(get_time("2023-06-30T17:27:23Z")),
1✔
207
        ..Default::default()
1✔
208
      },
1✔
209
      involved_object: ObjectReference::default(),
1✔
210
      series: Some(EventSeries {
1✔
211
        count: Some(3432),
1✔
212
        last_observed_time: Some(MicroTime(get_time("2023-06-30T17:27:23Z").0)),
1✔
213
      }),
1✔
214
      ..Default::default()
1✔
215
    };
1✔
216

217
    assert_eq!(KubeEvent::from(event).count, 3432);
1✔
218
  }
1✔
219

220
  #[test]
221
  fn test_event_age_prefers_latest_observed_timestamp() {
1✔
222
    let event = Event {
1✔
223
      metadata: ObjectMeta {
1✔
224
        creation_timestamp: Some(get_time("2023-06-30T17:27:23Z")),
1✔
225
        ..Default::default()
1✔
226
      },
1✔
227
      event_time: Some(MicroTime(get_time("2023-07-01T17:27:23Z").0)),
1✔
228
      last_timestamp: Some(get_time("2023-07-02T17:27:23Z")),
1✔
229
      involved_object: ObjectReference::default(),
1✔
230
      series: Some(EventSeries {
1✔
231
        count: Some(1),
1✔
232
        last_observed_time: Some(MicroTime(get_time("2023-07-03T17:27:23Z").0)),
1✔
233
      }),
1✔
234
      ..Default::default()
1✔
235
    };
1✔
236

237
    assert_eq!(
1✔
238
      KubeEvent::from(event).age,
1✔
239
      utils::to_age(Some(&get_time("2023-07-03T17:27:23Z")), Utc::now())
1✔
240
    );
241
  }
1✔
242

243
  fn get_micro_time(s: &str) -> Time {
1✔
244
    Time(s.parse().unwrap())
1✔
245
  }
1✔
246
}
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