Skip to content

Commit 4c25fc7

Browse files
committed
feat(metrics): enhance resource usage metrics for pods and nodes
- Introduced new functions to calculate total resource usage for pods by summing container metrics. - Added support for resource ratios (cpu_ratio, memory_ratio) in ResourceUsage. - Updated object_summary to include resource ratios based on requests and allocatable resources. - Implemented quantity_as_f64 to handle Kubernetes resource suffixes. - Added tests for pod usage calculations and resource ratio functionality. - Added relevant keywords to the Aetheris desktop entry and metainfo XML for improved discoverability. - Included multiple screenshots showcasing different features of the Aetheris application.
1 parent 072c501 commit 4c25fc7

22 files changed

Lines changed: 1084 additions & 561 deletions

CONTRIBUTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ Use `cargo check -p aetheris-app` or `cargo check -p aetheris-kube` while iterat
3939

4040
For documentation-only changes, a full Rust test run is usually not necessary. For workflow or Flatpak manifest changes, validate the affected file format and inspect shell scripts carefully.
4141

42+
## AI-Assisted Contributions
43+
44+
Contributions made with AI assistance are welcome, but the contributor remains
45+
responsible for the change. Do not submit code you do not understand. You must
46+
be able to explain what the code does, why it is correct, and what tradeoffs or
47+
risks it introduces.
48+
49+
AI-assisted changes must be tested thoroughly. Maintainers may ask for evidence
50+
that the functionality works and was tested, such as test output, screenshots,
51+
screen recordings, logs, or clear reproduction steps.
52+
4253
## Project Architecture
4354

4455
Aetheris has two Rust crates:

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,17 @@ scale, cordon, drain, and port forwarding.
3535

3636
## Screenshots
3737

38-
> Aetheris is under active development. Screenshots land here as the UI settles —
39-
> drop them in `data/screenshots/` and swap the placeholders below.
38+
| Resource browser |
39+
| :---: |
40+
| <img src="data/screenshots/resources.png" alt="Aetheris resource browser" /> |
4041

41-
| Cluster overview | Resource browser |
42+
| Cluster overview | Pod terminal |
4243
| :---: | :---: |
43-
| <!-- <img src="data/screenshots/overview.png" alt="Cluster overview" /> --> *coming soon* | <!-- <img src="data/screenshots/resources.png" alt="Resource browser" /> --> *coming soon* |
44+
| <img src="data/screenshots/overview.png" alt="Aetheris cluster overview" /> | <img src="data/screenshots/terminal.png" alt="Aetheris pod terminal" /> |
4445

45-
| YAML editor | Logs & pod terminal |
46+
| YAML editor | Live logs |
4647
| :---: | :---: |
47-
| <!-- <img src="data/screenshots/yaml.png" alt="YAML editor" /> --> *coming soon* | <!-- <img src="data/screenshots/terminal.png" alt="Logs and pod terminal" /> --> *coming soon* |
48+
| <img src="data/screenshots/yaml.png" alt="Aetheris YAML editor" /> | <img src="data/screenshots/logs.png" alt="Aetheris live logs" /> |
4849

4950
## Documentation
5051

crates/aetheris-app/src/app.rs

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{
77
use aetheris_kube::{
88
AddClusterRequest, ClusterSummary, ContainerUsage, ContextInfo, KubeManager, ObjectCondition,
99
ObjectDetail, ObjectEvent, ObjectSummary, ObjectWatchEvent, PodExecEvent, PodExecRequest,
10-
PodLogRequest, PodPortForwardEvent, PodPortForwardRequest, ResourceKind,
10+
PodLogRequest, PodPortForwardEvent, PodPortForwardRequest, ResourceKind, ResourceUsage,
1111
};
1212
use futures::future::{AbortHandle, Abortable};
1313
use futures::FutureExt;
@@ -197,6 +197,77 @@ flowboxchild.filter-chip-child:selected:hover {
197197
.filter-chip-active:hover {
198198
background-color: alpha(@accent_color, 0.24);
199199
}
200+
201+
/* Object tables: rounded card look, matching the old boxed-list style —
202+
* card background for rows, a distinct shaded header, hairline row
203+
* separators. The frame class goes on the enclosing ScrolledWindow so the
204+
* rounded corners wrap header and rows together. */
205+
scrolledwindow.aetheris-table-frame {
206+
background-color: @card_bg_color;
207+
border: 1px solid alpha(currentColor, 0.1);
208+
border-radius: 12px;
209+
}
210+
211+
columnview.aetheris-table,
212+
columnview.aetheris-table > listview {
213+
background: transparent;
214+
}
215+
216+
/* GNOME-style header: dimmed label at rest (like .dim-label), and on
217+
* hover a genuinely darker surface (@shade_color is Adwaita's translucent
218+
* black shading color in both light and dark) with the label brightening
219+
* to full strength. */
220+
columnview.aetheris-table > header > button {
221+
background-color: alpha(currentColor, 0.05);
222+
border-bottom: 1px solid alpha(currentColor, 0.1);
223+
padding: 8px 10px;
224+
font-weight: 700;
225+
font-size: 1em;
226+
color: alpha(@window_fg_color, 0.75);
227+
}
228+
229+
columnview.aetheris-table > header > button:first-child {
230+
border-top-left-radius: 11px;
231+
}
232+
233+
columnview.aetheris-table > header > button:last-child {
234+
border-top-right-radius: 11px;
235+
}
236+
237+
columnview.aetheris-table > header > button:hover {
238+
background-color: @shade_color;
239+
color: @window_fg_color;
240+
}
241+
242+
columnview.aetheris-table > header > button.sorted {
243+
background-color: alpha(@accent_color, 0.15);
244+
color: @accent_color;
245+
}
246+
247+
columnview.aetheris-table > listview > row {
248+
background: transparent;
249+
border-bottom: 1px solid alpha(currentColor, 0.06);
250+
padding: 2px 6px;
251+
}
252+
253+
columnview.aetheris-table > listview > row:hover {
254+
background-color: alpha(currentColor, 0.05);
255+
}
256+
257+
/* CPU/Memory usage bars (same thresholds as Seabird): the LevelBar picks
258+
* the class of the smallest offset >= its value — up to 85% "lb-normal",
259+
* up to 95% "lb-warning", above that "lb-error". */
260+
levelbar block.lb-normal {
261+
background-color: @view_fg_color;
262+
}
263+
264+
levelbar block.lb-warning {
265+
background-color: @warning_bg_color;
266+
}
267+
268+
levelbar block.lb-error {
269+
background-color: @destructive_bg_color;
270+
}
200271
"#;
201272

202273
#[derive(Debug, Clone)]
@@ -212,10 +283,15 @@ pub struct App {
212283
namespaces: Vec<String>,
213284
resources: Vec<ResourceKind>,
214285
objects: Vec<ObjectSummary>,
215-
/// Bumped on every `rebuild_object_list` call so a still-running
216-
/// chunked render of a stale list can detect it's obsolete and stop
217-
/// instead of appending rows after a newer rebuild already started.
218-
object_list_generation: std::rc::Rc<std::cell::Cell<u64>>,
286+
/// True while a coalesced object-list refresh is pending. Watch events
287+
/// can arrive thousands at a time while a namespace spins up; rebuilding
288+
/// the list per event would freeze the UI, so events only mark the list
289+
/// dirty and a single deferred refresh repaints it.
290+
object_list_refresh_scheduled: bool,
291+
/// True while a coalesced projects.json save is pending. Dragging a
292+
/// column border emits a width change per pixel; saving on each would
293+
/// hammer the disk, so width changes only schedule one deferred save.
294+
project_save_scheduled: bool,
219295
selected_context: Option<String>,
220296
selected_namespace: String,
221297
selected_resource_section: ResourceSection,
@@ -258,7 +334,16 @@ pub struct App {
258334
status_label: gtk::Label,
259335
spinner: gtk::Spinner,
260336
resource_list: gtk::ListBox,
261-
object_list: gtk::ListBox,
337+
/// Backing model for `object_view`. The `ColumnView` only realizes
338+
/// widgets for on-screen rows, so this can hold tens of thousands of
339+
/// objects without the widget tree growing with it.
340+
object_store: gtk::gio::ListStore,
341+
/// The store as the view displays it (header-click sort applied);
342+
/// activation positions index into this model.
343+
object_sorted: gtk::SortListModel,
344+
object_columns: Vec<(ObjectTableColumn, gtk::ColumnViewColumn)>,
345+
/// Switches between the object table and the "No objects" placeholder.
346+
object_list_stack: gtk::Stack,
262347
detail_stack: gtk::Stack,
263348
detail_name_label: gtk::Label,
264349
detail_namespace_label: gtk::Label,
@@ -281,7 +366,12 @@ pub struct App {
281366
detail_yaml_buffer: sourceview5::Buffer,
282367
detail_events_list: gtk::ListBox,
283368
detail_conditions_list: gtk::ListBox,
284-
detail_related_pods_list: gtk::ListBox,
369+
detail_related_pods_store: gtk::gio::ListStore,
370+
detail_related_pods_sorted: gtk::SortListModel,
371+
/// Switches the detail "Pods" tab between the pods table and an
372+
/// informational message (non-Deployment kinds, empty selector match).
373+
detail_related_pods_stack: gtk::Stack,
374+
detail_related_pods_message: adw::StatusPage,
285375
detail_log_container_dropdown: gtk::DropDown,
286376
detail_log_follow_check: gtk::CheckButton,
287377
detail_log_timestamps_check: gtk::CheckButton,
@@ -302,7 +392,6 @@ pub struct App {
302392
detail_log_target: Option<PodLogTarget>,
303393
detail_exec_target: Option<PodLogTarget>,
304394
detail_port_forward_target: Option<PodLogTarget>,
305-
detail_related_pods: Vec<ObjectSummary>,
306395
detail_node_unschedulable: Option<bool>,
307396
object_watch_token: u64,
308397
object_watch_abort_handle: Option<AbortHandle>,
@@ -398,6 +487,8 @@ pub enum AppMsg {
398487
ObjectCreated(Result<String, String>),
399488
ObjectWatchEvent(u64, ObjectWatchEvent),
400489
ObjectWatchFinished(u64, Result<(), String>),
490+
ObjectListRefreshTick,
491+
ProjectSaveTick,
401492
ScaleDeployment,
402493
ObjectScaled(u64, Result<ObjectDetail, String>),
403494
ToggleNodeScheduling,

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

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,46 @@ impl Component for App {
227227
let resource_list = gtk::ListBox::new();
228228
resource_list.add_css_class("boxed-list");
229229
resource_list.set_selection_mode(gtk::SelectionMode::None);
230-
let object_list = gtk::ListBox::new();
231-
object_list.add_css_class("boxed-list");
232-
object_list.set_selection_mode(gtk::SelectionMode::None);
230+
let object_store = gtk::gio::ListStore::new::<gtk::glib::BoxedAnyObject>();
231+
let object_view = gtk::ColumnView::builder()
232+
.single_click_activate(true)
233+
.reorderable(false)
234+
.build();
235+
object_view.add_css_class("aetheris-table");
236+
object_view.set_vexpand(true);
237+
238+
let mut object_columns: Vec<(ObjectTableColumn, gtk::ColumnViewColumn)> = Vec::new();
239+
let name_column = gtk::ColumnViewColumn::new(
240+
Some("Name"),
241+
Some(super::widgets::object_name_column_factory()),
242+
);
243+
name_column.set_resizable(true);
244+
name_column.set_fixed_width(OBJECT_NAME_WIDTH);
245+
object_view.append_column(&name_column);
246+
object_columns.push((ObjectTableColumn::Name, name_column));
247+
for column in ObjectColumn::ALL {
248+
let view_column = gtk::ColumnViewColumn::new(
249+
Some(column.label()),
250+
Some(super::widgets::object_data_column_factory(column)),
251+
);
252+
view_column.set_resizable(true);
253+
view_column.set_fixed_width(column.default_width());
254+
view_column.set_sorter(super::widgets::object_column_sorter(column).as_ref());
255+
object_view.append_column(&view_column);
256+
object_columns.push((ObjectTableColumn::Data(column), view_column));
257+
}
258+
super::widgets::append_filler_column(&object_view);
259+
// Header-click sorting reorders the view, not the store, so
260+
// activation positions must be resolved against this sorted model.
261+
let object_sorted =
262+
gtk::SortListModel::new(Some(object_store.clone()), object_view.sorter());
263+
object_view.set_model(Some(&gtk::NoSelection::new(Some(object_sorted.clone()))));
264+
super::widgets::connect_sorted_header_highlight(&object_view);
265+
266+
let object_list_stack = gtk::Stack::builder()
267+
.hhomogeneous(false)
268+
.vhomogeneous(false)
269+
.build();
233270
let detail_name_label = detail_value_label();
234271
let detail_namespace_label = detail_value_label();
235272
let detail_status_label = detail_value_label();
@@ -319,9 +356,18 @@ impl Component for App {
319356
let detail_conditions_list = gtk::ListBox::new();
320357
detail_conditions_list.add_css_class("boxed-list");
321358
detail_conditions_list.set_selection_mode(gtk::SelectionMode::None);
322-
let detail_related_pods_list = gtk::ListBox::new();
323-
detail_related_pods_list.add_css_class("boxed-list");
324-
detail_related_pods_list.set_selection_mode(gtk::SelectionMode::None);
359+
let (detail_related_pods_view, detail_related_pods_store, detail_related_pods_sorted) =
360+
super::widgets::related_pods_column_view();
361+
let detail_related_pods_stack = gtk::Stack::builder()
362+
.hhomogeneous(false)
363+
.vhomogeneous(false)
364+
.build();
365+
let detail_related_pods_message = adw::StatusPage::builder()
366+
.title("Pods are shown for Deployments")
367+
.description("Open a Deployment to inspect its related Pods.")
368+
.icon_name("dialog-information-symbolic")
369+
.build();
370+
detail_related_pods_message.add_css_class("compact");
325371
let detail_log_container_dropdown = gtk::DropDown::from_strings(&["No containers"]);
326372
detail_log_container_dropdown.set_sensitive(false);
327373
let detail_log_follow_check = gtk::CheckButton::builder().label("Follow").build();
@@ -413,7 +459,9 @@ impl Component for App {
413459
yaml_error_label: &detail_yaml_error_label,
414460
events_list: &detail_events_list,
415461
conditions_list: &detail_conditions_list,
416-
related_pods_list: &detail_related_pods_list,
462+
related_pods_view: &detail_related_pods_view,
463+
related_pods_stack: &detail_related_pods_stack,
464+
related_pods_message: &detail_related_pods_message,
417465
log_container_dropdown: &detail_log_container_dropdown,
418466
log_follow_check: &detail_log_follow_check,
419467
log_timestamps_check: &detail_log_timestamps_check,
@@ -539,7 +587,8 @@ impl Component for App {
539587
content_stack: &content_stack,
540588
status_label: &status_label,
541589
spinner: &spinner,
542-
object_list: &object_list,
590+
object_view: &object_view,
591+
object_list_stack: &object_list_stack,
543592
detail_page: &detail_page,
544593
});
545594
split_view.set_sidebar(Some(&sidebar));
@@ -687,14 +736,17 @@ impl Component for App {
687736
let sender = sender.clone();
688737
move |entry| sender.input(AppMsg::SearchChanged(entry.text().to_string()))
689738
});
690-
object_list.connect_row_activated({
739+
object_view.connect_activate({
691740
let sender = sender.clone();
692-
move |_, row| {
693-
if row.index() > 0 {
694-
sender.input(AppMsg::ObjectActivated(row.index() - 1));
695-
}
696-
}
741+
move |_, position| sender.input(AppMsg::ObjectActivated(position as i32))
697742
});
743+
for (table_column, view_column) in &object_columns {
744+
super::widgets::connect_object_column_persistence(
745+
view_column,
746+
*table_column,
747+
sender.clone(),
748+
);
749+
}
698750
create_yaml_button.connect_clicked({
699751
let sender = sender.clone();
700752
move |_| sender.input(AppMsg::ShowCreateYamlDialog)
@@ -703,9 +755,9 @@ impl Component for App {
703755
let sender = sender.clone();
704756
move |_| sender.input(AppMsg::CreateYaml)
705757
});
706-
detail_related_pods_list.connect_row_activated({
758+
detail_related_pods_view.connect_activate({
707759
let sender = sender.clone();
708-
move |_, row| sender.input(AppMsg::RelatedPodActivated(row.index()))
760+
move |_, position| sender.input(AppMsg::RelatedPodActivated(position as i32))
709761
});
710762
detail_back_button.connect_clicked({
711763
let sender = sender.clone();
@@ -802,7 +854,8 @@ impl Component for App {
802854
namespaces: vec![String::from("default")],
803855
resources: Vec::new(),
804856
objects: Vec::new(),
805-
object_list_generation: std::rc::Rc::new(std::cell::Cell::new(0)),
857+
object_list_refresh_scheduled: false,
858+
project_save_scheduled: false,
806859
selected_context: None,
807860
selected_namespace: String::from("default"),
808861
selected_resource_section: ResourceSection::Workloads,
@@ -842,7 +895,10 @@ impl Component for App {
842895
status_label,
843896
spinner,
844897
resource_list,
845-
object_list,
898+
object_store,
899+
object_sorted,
900+
object_columns,
901+
object_list_stack,
846902
detail_stack,
847903
detail_name_label,
848904
detail_namespace_label,
@@ -865,7 +921,10 @@ impl Component for App {
865921
detail_yaml_buffer,
866922
detail_events_list,
867923
detail_conditions_list,
868-
detail_related_pods_list,
924+
detail_related_pods_store,
925+
detail_related_pods_sorted,
926+
detail_related_pods_stack,
927+
detail_related_pods_message,
869928
detail_log_container_dropdown,
870929
detail_log_follow_check,
871930
detail_log_timestamps_check,
@@ -886,7 +945,6 @@ impl Component for App {
886945
detail_log_target: None,
887946
detail_exec_target: None,
888947
detail_port_forward_target: None,
889-
detail_related_pods: Vec::new(),
890948
detail_node_unschedulable: None,
891949
object_watch_token: 0,
892950
object_watch_abort_handle: None,

0 commit comments

Comments
 (0)