Skip to content

Commit 53b297d

Browse files
committed
feat: add deployment pod states section and styling to detail pane
1 parent cfe30d5 commit 53b297d

9 files changed

Lines changed: 220 additions & 13 deletions

File tree

crates/aetheris-app/src/app.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ pub(super) struct DetailPane {
9292
conditions_list: gtk::ListBox,
9393
related_pods_store: gtk::gio::ListStore,
9494
related_pods_sorted: gtk::SortListModel,
95+
related_pod_states_section: gtk::Box,
96+
related_pod_states: gtk::FlowBox,
9597
related_pods_stack: gtk::Stack,
9698
related_pods_message: adw::StatusPage,
9799
log_container_dropdown: gtk::DropDown,

crates/aetheris-app/src/app/component.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,17 @@ impl Component for App {
374374
detail_conditions_list.set_selection_mode(gtk::SelectionMode::None);
375375
let (detail_related_pods_view, detail_related_pods_store, detail_related_pods_sorted) =
376376
super::widgets::related_pods_column_view();
377+
let detail_related_pod_states = gtk::FlowBox::builder()
378+
.selection_mode(gtk::SelectionMode::None)
379+
.column_spacing(8)
380+
.row_spacing(8)
381+
.max_children_per_line(4)
382+
.min_children_per_line(1)
383+
.homogeneous(true)
384+
.hexpand(true)
385+
.build();
386+
detail_related_pod_states.add_css_class("deployment-pod-state-grid");
387+
let detail_related_pod_states_section = gtk::Box::new(gtk::Orientation::Vertical, 8);
377388
let detail_related_pods_stack = gtk::Stack::builder()
378389
.hhomogeneous(false)
379390
.vhomogeneous(false)
@@ -479,6 +490,8 @@ impl Component for App {
479490
events_list: &detail_events_list,
480491
conditions_list: &detail_conditions_list,
481492
related_pods_view: &detail_related_pods_view,
493+
related_pod_states_section: &detail_related_pod_states_section,
494+
related_pod_states: &detail_related_pod_states,
482495
related_pods_stack: &detail_related_pods_stack,
483496
related_pods_message: &detail_related_pods_message,
484497
log_container_dropdown: &detail_log_container_dropdown,
@@ -971,6 +984,8 @@ impl Component for App {
971984
conditions_list: detail_conditions_list,
972985
related_pods_store: detail_related_pods_store,
973986
related_pods_sorted: detail_related_pods_sorted,
987+
related_pod_states_section: detail_related_pod_states_section,
988+
related_pod_states: detail_related_pod_states,
974989
related_pods_stack: detail_related_pods_stack,
975990
related_pods_message: detail_related_pods_message,
976991
log_container_dropdown: detail_log_container_dropdown,

crates/aetheris-app/src/app/methods.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,11 @@ impl App {
930930
&self.detail.related_pods_message,
931931
detail,
932932
);
933+
rebuild_related_pod_states(
934+
&self.detail.related_pod_states_section,
935+
&self.detail.related_pod_states,
936+
detail,
937+
);
933938
rebuild_container_metrics(&self.detail.container_metrics_list, detail);
934939
self.sync_detail_tabs(detail);
935940
self.sync_terminal_controls();

crates/aetheris-app/src/app/object_detail.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ pub(super) struct ObjectDetailWidgets<'a> {
2525
pub(super) events_list: &'a gtk::ListBox,
2626
pub(super) conditions_list: &'a gtk::ListBox,
2727
pub(super) related_pods_view: &'a gtk::ColumnView,
28+
pub(super) related_pod_states_section: &'a gtk::Box,
29+
pub(super) related_pod_states: &'a gtk::FlowBox,
2830
pub(super) related_pods_stack: &'a gtk::Stack,
2931
pub(super) related_pods_message: &'a adw::StatusPage,
3032
pub(super) log_container_dropdown: &'a gtk::DropDown,
@@ -71,6 +73,17 @@ pub(super) fn build_object_detail_page(widgets: ObjectDetailWidgets<'_>) -> gtk:
7173
actions.append(widgets.drain_button);
7274
overview.append(&actions);
7375

76+
widgets
77+
.related_pod_states_section
78+
.add_css_class("deployment-pod-states");
79+
widgets
80+
.related_pod_states_section
81+
.append(&section_title(&tr("Pods by State")));
82+
widgets
83+
.related_pod_states_section
84+
.append(widgets.related_pod_states);
85+
overview.append(widgets.related_pod_states_section);
86+
7487
widgets
7588
.port_forward_group
7689
.append(&section_title(&tr("Port Forward")));
@@ -372,6 +385,49 @@ pub(super) fn rebuild_related_pods(
372385
stack.set_visible_child_name("table");
373386
}
374387

388+
pub(super) fn rebuild_related_pod_states(
389+
section: &gtk::Box,
390+
states: &gtk::FlowBox,
391+
detail: &ObjectDetail,
392+
) {
393+
while let Some(child) = states.first_child() {
394+
states.remove(&child);
395+
}
396+
397+
section.set_visible(detail.kind == "Deployment" && !detail.related_pod_states.is_empty());
398+
for state in &detail.related_pod_states {
399+
let card = gtk::Box::new(gtk::Orientation::Vertical, 2);
400+
card.add_css_class("pod-state-card");
401+
card.add_css_class(pod_state_css_class(&state.state));
402+
card.set_hexpand(true);
403+
card.set_size_request(170, -1);
404+
405+
let count = gtk::Label::builder()
406+
.label(state.count.to_string())
407+
.xalign(0.0)
408+
.css_classes(["title-1"])
409+
.build();
410+
let label = gtk::Label::builder()
411+
.label(&state.state)
412+
.xalign(0.0)
413+
.css_classes(["caption"])
414+
.build();
415+
card.append(&count);
416+
card.append(&label);
417+
states.insert(&card, -1);
418+
}
419+
}
420+
421+
fn pod_state_css_class(state: &str) -> &'static str {
422+
match state {
423+
"Running" => "pod-state-running",
424+
"Pending" => "pod-state-pending",
425+
"Succeeded" => "pod-state-succeeded",
426+
"Failed" => "pod-state-failed",
427+
_ => "pod-state-unknown",
428+
}
429+
}
430+
375431
pub(super) fn rebuild_container_metrics(list: &gtk::ListBox, detail: &ObjectDetail) {
376432
while let Some(child) = list.first_child() {
377433
list.remove(&child);

crates/aetheris-app/src/app/utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ pub(super) fn unavailable_object_detail(target: &DetailTarget) -> ObjectDetail {
296296
yaml: String::new(),
297297
containers: Vec::new(),
298298
related_pods: Vec::new(),
299+
related_pod_states: Vec::new(),
299300
replicas: None,
300301
node_unschedulable: None,
301302
conditions: Vec::new(),

crates/aetheris-kube/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ pub use session::KubeSession;
2020
pub use types::{
2121
AddClusterRequest, ClusterSummary, ContainerResources, ContainerUsage, ContextInfo,
2222
ObjectCondition, ObjectDetail, ObjectEvent, ObjectSummary, ObjectWatchEvent, PodExecEvent,
23-
PodExecRequest, PodLogRequest, PodPortForwardEvent, PodPortForwardRequest, PodSummary,
24-
ResourceKind, ResourceRatio, ResourceScope, ResourceUsage,
23+
PodExecRequest, PodLogRequest, PodPortForwardEvent, PodPortForwardRequest, PodStateCount,
24+
PodSummary, ResourceKind, ResourceRatio, ResourceScope, ResourceUsage,
2525
};

crates/aetheris-kube/src/objects.rs

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use serde_json::Value;
1111
use crate::status::{age_label, status_label};
1212
use crate::{
1313
ContainerResources, KubeSession, ObjectCondition, ObjectDetail, ObjectSummary,
14-
ObjectWatchEvent, PodSummary, ResourceKind, ResourceRatio, ResourceScope, ResourceUsage,
15-
api_resource, namespace_scope, resource_scope,
14+
ObjectWatchEvent, PodStateCount, PodSummary, ResourceKind, ResourceRatio, ResourceScope,
15+
ResourceUsage, api_resource, namespace_scope, resource_scope,
1616
};
1717

1818
impl KubeSession {
@@ -228,7 +228,7 @@ impl KubeSession {
228228
};
229229
let summary = object_summary(object.clone(), resource, &metrics);
230230
let yaml = serde_yaml::to_string(&object).context("failed to serialize object YAML")?;
231-
let related_pods = self
231+
let deployment_pods = self
232232
.deployment_pods(resource, &summary.namespace, &object)
233233
.await
234234
.unwrap_or_default();
@@ -253,7 +253,8 @@ impl KubeSession {
253253
container_resources,
254254
yaml,
255255
containers,
256-
related_pods,
256+
related_pods: deployment_pods.summaries,
257+
related_pod_states: deployment_pods.states,
257258
replicas,
258259
node_unschedulable,
259260
conditions,
@@ -267,12 +268,12 @@ impl KubeSession {
267268
resource: &ResourceKind,
268269
namespace: &str,
269270
object: &DynamicObject,
270-
) -> Result<Vec<ObjectSummary>> {
271+
) -> Result<DeploymentPods> {
271272
if resource.kind != "Deployment" || resource.group != "apps" || namespace == "-" {
272-
return Ok(Vec::new());
273+
return Ok(DeploymentPods::default());
273274
}
274275
let Some(selector) = deployment_label_selector(object) else {
275-
return Ok(Vec::new());
276+
return Ok(DeploymentPods::default());
276277
};
277278

278279
let pods: Api<Pod> = Api::namespaced(self.client.clone(), namespace);
@@ -288,7 +289,7 @@ impl KubeSession {
288289
.resource_metrics(&pod_resource, Some(namespace))
289290
.await
290291
.unwrap_or_default();
291-
let mut summaries = pods
292+
let pods = pods
292293
.list(&ListParams::default().labels(&selector))
293294
.await
294295
.with_context(|| {
@@ -297,7 +298,9 @@ impl KubeSession {
297298
object.name_any()
298299
)
299300
})?
300-
.items
301+
.items;
302+
let states = pod_state_counts(&pods);
303+
let mut summaries = pods
301304
.into_iter()
302305
.map(|pod| {
303306
let object = serde_json::to_value(&pod)
@@ -319,10 +322,46 @@ impl KubeSession {
319322
.collect::<Vec<_>>();
320323
summaries.sort_by(|left, right| left.name.cmp(&right.name));
321324

322-
Ok(summaries)
325+
Ok(DeploymentPods { summaries, states })
323326
}
324327
}
325328

329+
#[derive(Default)]
330+
struct DeploymentPods {
331+
summaries: Vec<ObjectSummary>,
332+
states: Vec<PodStateCount>,
333+
}
334+
335+
fn pod_state_counts(pods: &[Pod]) -> Vec<PodStateCount> {
336+
const ORDER: [&str; 5] = ["Running", "Pending", "Succeeded", "Failed", "Unknown"];
337+
338+
let mut counts = BTreeMap::<String, u32>::new();
339+
for pod in pods {
340+
let state = pod
341+
.status
342+
.as_ref()
343+
.and_then(|status| status.phase.as_deref())
344+
.unwrap_or("Unknown");
345+
*counts.entry(state.to_owned()).or_default() += 1;
346+
}
347+
348+
let mut states = ORDER
349+
.into_iter()
350+
.filter_map(|state| {
351+
counts.remove(state).map(|count| PodStateCount {
352+
state: state.to_owned(),
353+
count,
354+
})
355+
})
356+
.collect::<Vec<_>>();
357+
states.extend(
358+
counts
359+
.into_iter()
360+
.map(|(state, count)| PodStateCount { state, count }),
361+
);
362+
states
363+
}
364+
326365
fn sort_object_summaries(objects: &mut [ObjectSummary]) {
327366
objects.sort_by(|left, right| {
328367
left.namespace
@@ -651,8 +690,12 @@ fn deployment_label_selector(object: &DynamicObject) -> Option<String> {
651690

652691
#[cfg(test)]
653692
mod tests {
654-
use super::{object_container_resources, object_images, quantity_as_f64, resource_ratio};
693+
use super::{
694+
object_container_resources, object_images, pod_state_counts, quantity_as_f64,
695+
resource_ratio,
696+
};
655697
use crate::{ContainerResources, ResourceKind, ResourceScope};
698+
use k8s_openapi::api::core::v1::Pod;
656699
use kube::api::DynamicObject;
657700
use serde_json::json;
658701

@@ -782,4 +825,38 @@ mod tests {
782825
]
783826
);
784827
}
828+
829+
#[test]
830+
fn pod_state_counts_groups_phases_in_dashboard_order() {
831+
let pods: Vec<Pod> = serde_json::from_value(json!([
832+
{"apiVersion": "v1", "kind": "Pod", "status": {"phase": "Pending"}},
833+
{"apiVersion": "v1", "kind": "Pod", "status": {"phase": "Running"}},
834+
{"apiVersion": "v1", "kind": "Pod", "status": {"phase": "Running"}},
835+
{"apiVersion": "v1", "kind": "Pod", "status": {"phase": "Failed"}},
836+
{"apiVersion": "v1", "kind": "Pod", "status": {}}
837+
]))
838+
.expect("pods should deserialize");
839+
840+
assert_eq!(
841+
pod_state_counts(&pods),
842+
vec![
843+
crate::PodStateCount {
844+
state: String::from("Running"),
845+
count: 2
846+
},
847+
crate::PodStateCount {
848+
state: String::from("Pending"),
849+
count: 1
850+
},
851+
crate::PodStateCount {
852+
state: String::from("Failed"),
853+
count: 1
854+
},
855+
crate::PodStateCount {
856+
state: String::from("Unknown"),
857+
count: 1
858+
},
859+
]
860+
);
861+
}
785862
}

crates/aetheris-kube/src/types.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,22 @@ pub struct ObjectDetail {
8181
pub yaml: String,
8282
pub containers: Vec<String>,
8383
pub related_pods: Vec<ObjectSummary>,
84+
/// Counts of Pods selected by a Deployment, grouped by their Kubernetes
85+
/// lifecycle phase (Running, Pending, Succeeded, Failed, or Unknown).
86+
pub related_pod_states: Vec<PodStateCount>,
8487
pub replicas: Option<i32>,
8588
pub node_unschedulable: Option<bool>,
8689
pub conditions: Vec<ObjectCondition>,
8790
pub events: Vec<ObjectEvent>,
8891
pub events_error: Option<String>,
8992
}
9093

94+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95+
pub struct PodStateCount {
96+
pub state: String,
97+
pub count: u32,
98+
}
99+
91100
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92101
pub struct ObjectCondition {
93102
pub type_: String,

data/style.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,48 @@
4242
background-color: alpha(currentColor, 0.10);
4343
}
4444

45+
.deployment-pod-states {
46+
margin-top: 8px;
47+
}
48+
49+
.pod-state-card {
50+
border-radius: 10px;
51+
padding: 10px 14px;
52+
}
53+
54+
flowbox.deployment-pod-state-grid > flowboxchild,
55+
flowbox.deployment-pod-state-grid > flowboxchild:hover,
56+
flowbox.deployment-pod-state-grid > flowboxchild:selected,
57+
flowbox.deployment-pod-state-grid > flowboxchild:selected:hover {
58+
background: transparent;
59+
border-radius: 10px;
60+
box-shadow: none;
61+
}
62+
63+
.pod-state-running {
64+
background-color: alpha(@success_color, 0.16);
65+
color: @success_color;
66+
}
67+
68+
.pod-state-pending {
69+
background-color: alpha(@warning_color, 0.18);
70+
color: @warning_color;
71+
}
72+
73+
.pod-state-succeeded {
74+
background-color: alpha(@accent_color, 0.15);
75+
color: @accent_color;
76+
}
77+
78+
.pod-state-failed {
79+
background-color: alpha(@error_color, 0.16);
80+
color: @error_color;
81+
}
82+
83+
.pod-state-unknown {
84+
background-color: alpha(currentColor, 0.10);
85+
}
86+
4587
.status-icon-good {
4688
color: @success_color;
4789
}

0 commit comments

Comments
 (0)