Skip to content

Commit 939385d

Browse files
committed
feat(objects): show pod image column
Collect container images for Pod summaries and expose them through ObjectSummary with backward-compatible serde defaults. Add a Pod-only Image column with sorting, search, related-pod support, persisted column migration, image shortening, and tests for display rules and table width bounds.
1 parent 244f1c9 commit 939385d

7 files changed

Lines changed: 295 additions & 28 deletions

File tree

crates/aetheris-app/src/app.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,14 @@ use widgets::{
4747

4848
const DEFAULT_PROJECT_NAME: &str = "Default";
4949
const OBJECT_NAME_WIDTH: i32 = 244;
50+
const OBJECT_IMAGE_WIDTH: i32 = 180;
5051
const OBJECT_NAMESPACE_WIDTH: i32 = 132;
5152
const OBJECT_STATUS_WIDTH: i32 = 64;
5253
const OBJECT_METRIC_WIDTH: i32 = 64;
5354
const OBJECT_API_WIDTH: i32 = 96;
5455
const OBJECT_AGE_WIDTH: i32 = 56;
5556
const OBJECT_COLUMN_MIN_WIDTH: i32 = 48;
56-
const OBJECT_COLUMN_MAX_WIDTH: i32 = 260;
5757
const OBJECT_NAME_MIN_WIDTH: i32 = 160;
58-
const OBJECT_NAME_MAX_WIDTH: i32 = 520;
5958

6059
#[derive(Debug, Clone)]
6160
pub(super) enum ClusterSummaryState {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ impl App {
125125
}
126126

127127
pub(super) fn project_contexts(&self) -> Vec<&ContextInfo> {
128+
if self.projects.projects.is_empty() {
129+
return Vec::new();
130+
}
131+
128132
let Some(project) = self.projects.selected_project() else {
129133
return self.contexts.iter().collect();
130134
};

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

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ pub struct ProjectStore {
66
pub(super) selected_project: Option<String>,
77
#[serde(default)]
88
pub(super) last_namespaces_by_context: Vec<ContextNamespaceSelection>,
9+
#[serde(default)]
10+
pub(super) object_column_schema_version: u32,
911
#[serde(default = "default_object_columns")]
1012
pub(super) visible_object_columns: Vec<ObjectColumn>,
1113
#[serde(default)]
@@ -73,6 +75,7 @@ pub(super) enum StatusFilter {
7375
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7476
#[serde(rename_all = "snake_case")]
7577
pub(crate) enum ObjectColumn {
78+
Image,
7679
Namespace,
7780
Status,
7881
Cpu,
@@ -94,7 +97,8 @@ pub(super) struct ObjectColumnWidth {
9497
}
9598

9699
impl ObjectColumn {
97-
pub(super) const ALL: [Self; 6] = [
100+
pub(super) const ALL: [Self; 7] = [
101+
Self::Image,
98102
Self::Namespace,
99103
Self::Status,
100104
Self::Cpu,
@@ -105,6 +109,7 @@ impl ObjectColumn {
105109

106110
pub(super) fn label(self) -> String {
107111
match self {
112+
Self::Image => tr("Image"),
108113
Self::Namespace => tr("Namespace"),
109114
Self::Status => tr("Status"),
110115
Self::Cpu => tr("CPU"),
@@ -116,6 +121,7 @@ impl ObjectColumn {
116121

117122
pub(super) fn default_width(self) -> i32 {
118123
match self {
124+
Self::Image => OBJECT_IMAGE_WIDTH,
119125
Self::Namespace => OBJECT_NAMESPACE_WIDTH,
120126
Self::Status => OBJECT_STATUS_WIDTH,
121127
Self::Cpu | Self::Memory => OBJECT_METRIC_WIDTH,
@@ -129,6 +135,8 @@ pub(super) fn default_object_columns() -> Vec<ObjectColumn> {
129135
ObjectColumn::ALL.to_vec()
130136
}
131137

138+
const OBJECT_COLUMN_SCHEMA_VERSION: u32 = 1;
139+
132140
impl StatusFilter {
133141
pub(super) const ALL: [Self; 5] = [
134142
Self::Ready,
@@ -251,6 +259,24 @@ impl ResourceSection {
251259

252260
impl Default for ProjectStore {
253261
fn default() -> Self {
262+
Self::with_default_project()
263+
}
264+
}
265+
266+
impl ProjectStore {
267+
fn empty() -> Self {
268+
Self {
269+
projects: Vec::new(),
270+
selected_project: None,
271+
last_namespaces_by_context: Vec::new(),
272+
object_column_schema_version: OBJECT_COLUMN_SCHEMA_VERSION,
273+
visible_object_columns: default_object_columns(),
274+
object_name_width: None,
275+
object_column_widths: Vec::new(),
276+
}
277+
}
278+
279+
fn with_default_project() -> Self {
254280
Self {
255281
projects: vec![Project {
256282
name: String::from(DEFAULT_PROJECT_NAME),
@@ -259,16 +285,18 @@ impl Default for ProjectStore {
259285
}],
260286
selected_project: Some(String::from(DEFAULT_PROJECT_NAME)),
261287
last_namespaces_by_context: Vec::new(),
288+
object_column_schema_version: OBJECT_COLUMN_SCHEMA_VERSION,
262289
visible_object_columns: default_object_columns(),
263290
object_name_width: None,
264291
object_column_widths: Vec::new(),
265292
}
266293
}
267-
}
268294

269-
impl ProjectStore {
270295
pub(super) fn load(contexts: &[ContextInfo]) -> Self {
271-
let mut store = Self::read_from_disk().unwrap_or_default();
296+
let Some(mut store) = Self::read_from_disk() else {
297+
return Self::empty();
298+
};
299+
store.migrate_object_columns();
272300
store.normalize_object_columns();
273301
store.normalize_object_column_widths();
274302
store.normalize_object_name_width();
@@ -315,13 +343,13 @@ impl ProjectStore {
315343
.find(|entry| entry.column == column)
316344
.map(|entry| entry.width)
317345
.unwrap_or_else(|| column.default_width())
318-
.clamp(OBJECT_COLUMN_MIN_WIDTH, OBJECT_COLUMN_MAX_WIDTH)
346+
.max(OBJECT_COLUMN_MIN_WIDTH)
319347
}
320348

321349
pub(super) fn object_name_width(&self) -> i32 {
322350
self.object_name_width
323351
.unwrap_or(OBJECT_NAME_WIDTH)
324-
.clamp(OBJECT_NAME_MIN_WIDTH, OBJECT_NAME_MAX_WIDTH)
352+
.max(OBJECT_NAME_MIN_WIDTH)
325353
}
326354

327355
pub(super) fn set_object_table_column_width(
@@ -376,7 +404,7 @@ impl ProjectStore {
376404
}
377405

378406
fn set_object_name_width(&mut self, width: i32) -> bool {
379-
let width = width.clamp(OBJECT_NAME_MIN_WIDTH, OBJECT_NAME_MAX_WIDTH);
407+
let width = width.max(OBJECT_NAME_MIN_WIDTH);
380408
let next = (width != OBJECT_NAME_WIDTH).then_some(width);
381409
if self.object_name_width == next {
382410
return false;
@@ -386,7 +414,7 @@ impl ProjectStore {
386414
}
387415

388416
pub(super) fn set_object_column_width(&mut self, column: ObjectColumn, width: i32) -> bool {
389-
let width = width.clamp(OBJECT_COLUMN_MIN_WIDTH, OBJECT_COLUMN_MAX_WIDTH);
417+
let width = width.max(OBJECT_COLUMN_MIN_WIDTH);
390418
if width == column.default_width() {
391419
let previous_len = self.object_column_widths.len();
392420
self.object_column_widths
@@ -424,13 +452,20 @@ impl ProjectStore {
424452
self.visible_object_columns.dedup();
425453
}
426454

455+
fn migrate_object_columns(&mut self) {
456+
if self.object_column_schema_version < 1
457+
&& !self.visible_object_columns.contains(&ObjectColumn::Image)
458+
{
459+
self.visible_object_columns.push(ObjectColumn::Image);
460+
}
461+
self.object_column_schema_version = OBJECT_COLUMN_SCHEMA_VERSION;
462+
}
463+
427464
fn normalize_object_column_widths(&mut self) {
428465
self.object_column_widths
429466
.retain(|entry| ObjectColumn::ALL.contains(&entry.column));
430467
for entry in &mut self.object_column_widths {
431-
entry.width = entry
432-
.width
433-
.clamp(OBJECT_COLUMN_MIN_WIDTH, OBJECT_COLUMN_MAX_WIDTH);
468+
entry.width = entry.width.max(OBJECT_COLUMN_MIN_WIDTH);
434469
}
435470
self.object_column_widths.sort_by_key(|entry| {
436471
ObjectColumn::ALL
@@ -446,7 +481,7 @@ impl ProjectStore {
446481
fn normalize_object_name_width(&mut self) {
447482
self.object_name_width = self
448483
.object_name_width
449-
.map(|width| width.clamp(OBJECT_NAME_MIN_WIDTH, OBJECT_NAME_MAX_WIDTH))
484+
.map(|width| width.max(OBJECT_NAME_MIN_WIDTH))
450485
.filter(|width| *width != OBJECT_NAME_WIDTH);
451486
}
452487

@@ -490,15 +525,6 @@ impl ProjectStore {
490525
}
491526

492527
pub(super) fn normalize_contexts(&mut self, contexts: &[ContextInfo]) {
493-
if self.projects.is_empty() {
494-
self.projects.push(Project {
495-
name: String::from(DEFAULT_PROJECT_NAME),
496-
contexts: Vec::new(),
497-
custom_namespaces_by_context: Vec::new(),
498-
});
499-
self.selected_project = Some(String::from(DEFAULT_PROJECT_NAME));
500-
}
501-
502528
// The kubeconfig can be changed by kubectl/oc outside Aetheris. Keep
503529
// only clusters explicitly saved in projects.json; use the live
504530
// kubeconfig here only to prune deleted/renamed entries. An empty
@@ -746,6 +772,7 @@ mod tests {
746772
}],
747773
selected_project: Some(String::from("Work")),
748774
last_namespaces_by_context: Vec::new(),
775+
object_column_schema_version: OBJECT_COLUMN_SCHEMA_VERSION,
749776
visible_object_columns: default_object_columns(),
750777
object_name_width: None,
751778
object_column_widths: Vec::new(),
@@ -757,6 +784,16 @@ mod tests {
757784
assert_eq!(project.contexts, vec![String::from("local")]);
758785
}
759786

787+
#[test]
788+
fn normalize_contexts_keeps_empty_store_empty() {
789+
let mut store = ProjectStore::empty();
790+
791+
store.normalize_contexts(&[context("prod")]);
792+
793+
assert!(store.projects.is_empty());
794+
assert_eq!(store.selected_project, None);
795+
}
796+
760797
#[test]
761798
fn normalize_contexts_keeps_saved_contexts_when_live_list_is_empty() {
762799
let mut store = ProjectStore {
@@ -773,6 +810,7 @@ mod tests {
773810
context: String::from("prod"),
774811
namespace: String::from("team-a"),
775812
}],
813+
object_column_schema_version: OBJECT_COLUMN_SCHEMA_VERSION,
776814
visible_object_columns: default_object_columns(),
777815
object_name_width: None,
778816
object_column_widths: Vec::new(),
@@ -813,6 +851,35 @@ mod tests {
813851
assert_eq!(store.last_namespaces_by_context.len(), 1);
814852
}
815853

854+
#[test]
855+
fn object_table_columns_have_no_maximum_width() {
856+
let mut store = ProjectStore::default();
857+
let wide = 10_000;
858+
859+
assert!(store.set_object_column_width(ObjectColumn::Image, wide));
860+
assert_eq!(store.object_column_width(ObjectColumn::Image), wide);
861+
862+
assert!(store.set_object_column_width(ObjectColumn::Namespace, wide));
863+
assert_eq!(store.object_column_width(ObjectColumn::Namespace), wide);
864+
865+
assert!(store.set_object_table_column_width(ObjectTableColumn::Name, wide));
866+
assert_eq!(store.object_name_width(), wide);
867+
}
868+
869+
#[test]
870+
fn object_table_columns_keep_minimum_width() {
871+
let mut store = ProjectStore::default();
872+
873+
assert!(store.set_object_column_width(ObjectColumn::Image, 0));
874+
assert_eq!(
875+
store.object_column_width(ObjectColumn::Image),
876+
OBJECT_COLUMN_MIN_WIDTH
877+
);
878+
879+
assert!(store.set_object_table_column_width(ObjectTableColumn::Name, 0));
880+
assert_eq!(store.object_name_width(), OBJECT_NAME_MIN_WIDTH);
881+
}
882+
816883
#[test]
817884
fn normalize_contexts_prunes_last_namespaces_for_deleted_contexts() {
818885
let mut store = ProjectStore {
@@ -832,6 +899,7 @@ mod tests {
832899
namespace: String::from("team-b"),
833900
},
834901
],
902+
object_column_schema_version: OBJECT_COLUMN_SCHEMA_VERSION,
835903
visible_object_columns: default_object_columns(),
836904
object_name_width: None,
837905
object_column_widths: Vec::new(),

0 commit comments

Comments
 (0)