@@ -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" ) ]
7577pub ( crate ) enum ObjectColumn {
78+ Image ,
7679 Namespace ,
7780 Status ,
7881 Cpu ,
@@ -94,7 +97,8 @@ pub(super) struct ObjectColumnWidth {
9497}
9598
9699impl 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+
132140impl StatusFilter {
133141 pub ( super ) const ALL : [ Self ; 5 ] = [
134142 Self :: Ready ,
@@ -251,6 +259,24 @@ impl ResourceSection {
251259
252260impl 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