Skip to content

Commit 1223d4d

Browse files
committed
feat: implement object caching mechanism and enhance status filter options
1 parent 5f8f95c commit 1223d4d

7 files changed

Lines changed: 123 additions & 7 deletions

File tree

crates/aetheris-app/src/app.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
collections::{BTreeSet, HashMap},
2+
collections::{BTreeSet, HashMap, VecDeque},
33
fs,
44
path::PathBuf,
55
};
@@ -56,6 +56,7 @@ const OBJECT_API_WIDTH: i32 = 96;
5656
const OBJECT_AGE_WIDTH: i32 = 56;
5757
const OBJECT_COLUMN_MIN_WIDTH: i32 = 48;
5858
const OBJECT_NAME_MIN_WIDTH: i32 = 160;
59+
const OBJECT_CACHE_LIMIT: usize = 20;
5960

6061
#[derive(Debug, Clone)]
6162
pub(super) enum ClusterSummaryState {
@@ -180,6 +181,8 @@ pub struct App {
180181
object_sorted: gtk::SortListModel,
181182
object_columns: Vec<(ObjectTableColumn, gtk::ColumnViewColumn)>,
182183
object_list_stack: gtk::Stack,
184+
object_cache: HashMap<ObjectCacheKey, Vec<ObjectSummary>>,
185+
object_cache_order: VecDeque<ObjectCacheKey>,
183186
detail: DetailPane,
184187
object_load_token: u64,
185188
object_watch_token: u64,
@@ -221,6 +224,16 @@ pub struct App {
221224
renaming_namespace: Option<String>,
222225
}
223226

227+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
228+
struct ObjectCacheKey {
229+
context: String,
230+
group: String,
231+
version: String,
232+
kind: String,
233+
plural: String,
234+
namespace: Option<String>,
235+
}
236+
224237
#[cfg(not(target_os = "windows"))]
225238
struct TerminalSession {
226239
window: adw::Window,

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use super::utils::*;
44
use super::*;
55

66
const CLUSTER_LOAD_TIMEOUT: Duration = Duration::from_secs(30);
7+
const OBJECT_LIST_TIMEOUT: Duration = Duration::from_secs(30);
78

89
pub(super) async fn load_state() -> AppMsg {
910
let result = async {
@@ -89,7 +90,17 @@ pub(super) async fn list_objects(
8990
resource: ResourceKind,
9091
namespace: Option<String>,
9192
) -> AppMsg {
92-
let result = list_objects_snapshot(context, resource, namespace).await;
93+
let result = match tokio::time::timeout(
94+
OBJECT_LIST_TIMEOUT,
95+
list_objects_snapshot(context, resource, namespace),
96+
)
97+
.await
98+
{
99+
Ok(result) => result,
100+
Err(_) => Err(String::from(
101+
"Timed out while listing objects for the selected resource. Check that the API server is reachable and try again.",
102+
)),
103+
};
93104

94105
AppMsg::ObjectsLoaded(token, result)
95106
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,8 @@ impl Component for App {
942942
object_sorted,
943943
object_columns,
944944
object_list_stack,
945+
object_cache: HashMap::new(),
946+
object_cache_order: VecDeque::new(),
945947
detail: DetailPane {
946948
back_button: detail_back_button,
947949
stack: detail_stack,

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@ impl App {
686686
}
687687
AppMsg::ObjectCreated(Ok(name)) => {
688688
self.loading = false;
689+
self.clear_object_cache();
689690
self.create_yaml_dialog.close();
690691
self.create_yaml_buffer.set_text("");
691692
self.toaster.add_toast(adw::Toast::new(&tr_format(
@@ -722,6 +723,7 @@ impl App {
722723
return;
723724
}
724725
self.loading = false;
726+
self.clear_object_cache();
725727
self.status = tr_format("Scaled {name}", &[("{name}", detail.name.clone())]);
726728
self.populate_detail_dialog(&detail);
727729
self.update_log_target_containers(&detail);
@@ -761,6 +763,7 @@ impl App {
761763
return;
762764
}
763765
self.loading = false;
766+
self.clear_object_cache();
764767
self.status = tr_format("Updated {name}", &[("{name}", detail.name.clone())]);
765768
self.populate_detail_dialog(&detail);
766769
self.update_log_target_containers(&detail);
@@ -822,6 +825,7 @@ impl App {
822825
return;
823826
}
824827
self.loading = false;
828+
self.clear_object_cache();
825829
self.status = tr_format("Drained {name}", &[("{name}", detail.name.clone())]);
826830
self.populate_detail_dialog(&detail);
827831
self.update_log_target_containers(&detail);
@@ -861,6 +865,7 @@ impl App {
861865
return;
862866
}
863867
self.loading = false;
868+
self.clear_object_cache();
864869
self.status = tr_format("Applied {name}", &[("{name}", detail.name.clone())]);
865870
self.populate_detail_dialog(&detail);
866871
self.update_log_target_containers(&detail);
@@ -1014,6 +1019,7 @@ impl App {
10141019
return;
10151020
}
10161021
self.loading = false;
1022+
self.clear_object_cache();
10171023
self.detail.target = None;
10181024
self.detail.log_target = None;
10191025
self.detail.exec_target = None;
@@ -1162,6 +1168,7 @@ impl App {
11621168
self.loading = false;
11631169
let count = objects.len();
11641170
self.objects = objects;
1171+
self.cache_current_objects();
11651172
self.set_object_status(count);
11661173
self.sync_status();
11671174
self.rebuild_object_list();
@@ -1173,7 +1180,14 @@ impl App {
11731180
}
11741181
self.loading = false;
11751182
self.stop_object_watch();
1176-
self.objects.clear();
1183+
if let Some(objects) = self
1184+
.current_object_cache_key()
1185+
.and_then(|key| self.cached_objects(&key))
1186+
{
1187+
self.objects = objects;
1188+
} else {
1189+
self.objects.clear();
1190+
}
11771191
self.status = tr("Unable to list selected resource.");
11781192
self.sync_status();
11791193
self.rebuild_object_list();
@@ -1186,14 +1200,17 @@ impl App {
11861200
match event {
11871201
ObjectWatchEvent::Restarted(objects) => {
11881202
self.objects = objects;
1203+
self.cache_current_objects();
11891204
self.schedule_object_list_refresh(&sender);
11901205
}
11911206
ObjectWatchEvent::Applied(object) => {
11921207
self.upsert_object(object);
1208+
self.cache_current_objects();
11931209
self.schedule_object_list_refresh(&sender);
11941210
}
11951211
ObjectWatchEvent::Deleted(object) => {
11961212
self.remove_object(&object);
1213+
self.cache_current_objects();
11971214
self.schedule_object_list_refresh(&sender);
11981215
}
11991216
ObjectWatchEvent::Error(error) => {

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

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ impl App {
232232
self.stop_object_watch();
233233
self.stop_log_stream();
234234
self.stop_port_forward();
235+
self.object_cache.clear();
236+
self.object_cache_order.clear();
235237
self.detail.exec_target = None;
236238
self.detail.port_forward_target = None;
237239
self.loading = true;
@@ -275,9 +277,20 @@ impl App {
275277
self.stop_object_watch();
276278
self.object_load_token = self.object_load_token.saturating_add(1);
277279
let token = self.object_load_token;
280+
let cache_key = object_cache_key(&context, &resource, namespace.clone());
278281
self.loading = true;
279-
self.objects.clear();
280-
self.status = tr_format("Loading {resource}...", &[("{resource}", resource.label())]);
282+
if let Some(objects) = self.cached_objects(&cache_key) {
283+
let count = objects.len();
284+
self.objects = objects;
285+
self.set_object_status(count);
286+
self.status = tr_format(
287+
"Refreshing {resource}...",
288+
&[("{resource}", resource.label())],
289+
);
290+
} else {
291+
self.objects.clear();
292+
self.status = tr_format("Loading {resource}...", &[("{resource}", resource.label())]);
293+
}
281294
self.sync_status();
282295
self.rebuild_object_list();
283296
sender.oneshot_command(
@@ -682,6 +695,47 @@ impl App {
682695
.splice(0, self.object_store.n_items(), &items);
683696
}
684697

698+
pub(super) fn current_object_cache_key(&self) -> Option<ObjectCacheKey> {
699+
let context = self.selected_context.as_ref()?;
700+
let resource = self.selected_resource_kind()?;
701+
let namespace = resource
702+
.is_namespaced()
703+
.then(|| self.selected_namespace.clone());
704+
Some(object_cache_key(context, resource, namespace))
705+
}
706+
707+
pub(super) fn cached_objects(&mut self, key: &ObjectCacheKey) -> Option<Vec<ObjectSummary>> {
708+
let objects = self.object_cache.get(key)?.clone();
709+
self.touch_object_cache_key(key.clone());
710+
Some(objects)
711+
}
712+
713+
pub(super) fn cache_current_objects(&mut self) {
714+
if let Some(key) = self.current_object_cache_key() {
715+
self.cache_objects(key, self.objects.clone());
716+
}
717+
}
718+
719+
pub(super) fn cache_objects(&mut self, key: ObjectCacheKey, objects: Vec<ObjectSummary>) {
720+
self.object_cache.insert(key.clone(), objects);
721+
self.touch_object_cache_key(key);
722+
while self.object_cache_order.len() > OBJECT_CACHE_LIMIT {
723+
if let Some(oldest) = self.object_cache_order.pop_front() {
724+
self.object_cache.remove(&oldest);
725+
}
726+
}
727+
}
728+
729+
pub(super) fn touch_object_cache_key(&mut self, key: ObjectCacheKey) {
730+
self.object_cache_order.retain(|existing| existing != &key);
731+
self.object_cache_order.push_back(key);
732+
}
733+
734+
pub(super) fn clear_object_cache(&mut self) {
735+
self.object_cache.clear();
736+
self.object_cache_order.clear();
737+
}
738+
685739
/// Merges one watch event into `objects` without sorting or repainting;
686740
/// both are deferred to `flush_object_list_refresh` so an event burst
687741
/// costs one refresh instead of thousands.
@@ -941,6 +995,21 @@ impl App {
941995
}
942996
}
943997

998+
fn object_cache_key(
999+
context: &str,
1000+
resource: &ResourceKind,
1001+
namespace: Option<String>,
1002+
) -> ObjectCacheKey {
1003+
ObjectCacheKey {
1004+
context: context.to_owned(),
1005+
group: resource.group.clone(),
1006+
version: resource.version.clone(),
1007+
kind: resource.kind.clone(),
1008+
plural: resource.plural.clone(),
1009+
namespace,
1010+
}
1011+
}
1012+
9441013
/// The object at a view position, resolved against the sorted model the
9451014
/// `ColumnView` actually displays (positions differ from the backing store
9461015
/// whenever a header-click sort is active).

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub(crate) enum ResourceSection {
6868
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
6969
pub(super) enum StatusFilter {
7070
Ready,
71+
Available,
7172
Unavailable,
7273
Running,
7374
Pending,
@@ -198,8 +199,9 @@ pub(super) fn default_object_columns() -> Vec<ObjectColumn> {
198199
const OBJECT_COLUMN_SCHEMA_VERSION: u32 = 1;
199200

200201
impl StatusFilter {
201-
pub(super) const ALL: [Self; 5] = [
202+
pub(super) const ALL: [Self; 6] = [
202203
Self::Ready,
204+
Self::Available,
203205
Self::Unavailable,
204206
Self::Running,
205207
Self::Pending,
@@ -209,6 +211,7 @@ impl StatusFilter {
209211
pub(super) fn label(self) -> String {
210212
match self {
211213
Self::Ready => tr("Ready"),
214+
Self::Available => tr("Available"),
212215
Self::Unavailable => tr("Unavailable"),
213216
Self::Running => tr("Running"),
214217
Self::Pending => tr("Pending"),
@@ -237,6 +240,7 @@ impl StatusFilter {
237240
pub(super) fn keyword(self) -> &'static str {
238241
match self {
239242
Self::Ready => "Ready",
243+
Self::Available => "Available",
240244
Self::Unavailable => "Unavailable",
241245
Self::Running => "Running",
242246
Self::Pending => "Pending",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ fn filter_chip(
295295

296296
fn status_filter_tone(filter: StatusFilter) -> StatusTone {
297297
match filter {
298-
StatusFilter::Ready | StatusFilter::Running => StatusTone::Good,
298+
StatusFilter::Ready | StatusFilter::Available | StatusFilter::Running => StatusTone::Good,
299299
StatusFilter::Pending | StatusFilter::Unavailable => StatusTone::Warning,
300300
StatusFilter::Failed => StatusTone::Bad,
301301
}

0 commit comments

Comments
 (0)