Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion crates/aetheris-app/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{
collections::{BTreeSet, HashMap},
collections::{BTreeSet, HashMap, VecDeque},
fs,
path::PathBuf,
};
Expand Down Expand Up @@ -56,6 +56,7 @@ const OBJECT_API_WIDTH: i32 = 96;
const OBJECT_AGE_WIDTH: i32 = 56;
const OBJECT_COLUMN_MIN_WIDTH: i32 = 48;
const OBJECT_NAME_MIN_WIDTH: i32 = 160;
const OBJECT_CACHE_LIMIT: usize = 20;

#[derive(Debug, Clone)]
pub(super) enum ClusterSummaryState {
Expand Down Expand Up @@ -180,6 +181,8 @@ pub struct App {
object_sorted: gtk::SortListModel,
object_columns: Vec<(ObjectTableColumn, gtk::ColumnViewColumn)>,
object_list_stack: gtk::Stack,
object_cache: HashMap<ObjectCacheKey, Vec<ObjectSummary>>,
object_cache_order: VecDeque<ObjectCacheKey>,
detail: DetailPane,
object_load_token: u64,
object_watch_token: u64,
Expand Down Expand Up @@ -221,6 +224,16 @@ pub struct App {
renaming_namespace: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ObjectCacheKey {
context: String,
group: String,
version: String,
kind: String,
plural: String,
namespace: Option<String>,
}

#[cfg(not(target_os = "windows"))]
struct TerminalSession {
window: adw::Window,
Expand Down
13 changes: 12 additions & 1 deletion crates/aetheris-app/src/app/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::utils::*;
use super::*;

const CLUSTER_LOAD_TIMEOUT: Duration = Duration::from_secs(30);
const OBJECT_LIST_TIMEOUT: Duration = Duration::from_secs(30);

pub(super) async fn load_state() -> AppMsg {
let result = async {
Expand Down Expand Up @@ -89,7 +90,17 @@ pub(super) async fn list_objects(
resource: ResourceKind,
namespace: Option<String>,
) -> AppMsg {
let result = list_objects_snapshot(context, resource, namespace).await;
let result = match tokio::time::timeout(
OBJECT_LIST_TIMEOUT,
list_objects_snapshot(context, resource, namespace),
)
.await
{
Ok(result) => result,
Err(_) => Err(String::from(
"Timed out while listing objects for the selected resource. Check that the API server is reachable and try again.",
)),
};

AppMsg::ObjectsLoaded(token, result)
}
Expand Down
2 changes: 2 additions & 0 deletions crates/aetheris-app/src/app/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,8 @@ impl Component for App {
object_sorted,
object_columns,
object_list_stack,
object_cache: HashMap::new(),
object_cache_order: VecDeque::new(),
detail: DetailPane {
back_button: detail_back_button,
stack: detail_stack,
Expand Down
19 changes: 18 additions & 1 deletion crates/aetheris-app/src/app/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@ impl App {
}
AppMsg::ObjectCreated(Ok(name)) => {
self.loading = false;
self.clear_object_cache();
self.create_yaml_dialog.close();
self.create_yaml_buffer.set_text("");
self.toaster.add_toast(adw::Toast::new(&tr_format(
Expand Down Expand Up @@ -722,6 +723,7 @@ impl App {
return;
}
self.loading = false;
self.clear_object_cache();
self.status = tr_format("Scaled {name}", &[("{name}", detail.name.clone())]);
self.populate_detail_dialog(&detail);
self.update_log_target_containers(&detail);
Expand Down Expand Up @@ -761,6 +763,7 @@ impl App {
return;
}
self.loading = false;
self.clear_object_cache();
self.status = tr_format("Updated {name}", &[("{name}", detail.name.clone())]);
self.populate_detail_dialog(&detail);
self.update_log_target_containers(&detail);
Expand Down Expand Up @@ -822,6 +825,7 @@ impl App {
return;
}
self.loading = false;
self.clear_object_cache();
self.status = tr_format("Drained {name}", &[("{name}", detail.name.clone())]);
self.populate_detail_dialog(&detail);
self.update_log_target_containers(&detail);
Expand Down Expand Up @@ -861,6 +865,7 @@ impl App {
return;
}
self.loading = false;
self.clear_object_cache();
self.status = tr_format("Applied {name}", &[("{name}", detail.name.clone())]);
self.populate_detail_dialog(&detail);
self.update_log_target_containers(&detail);
Expand Down Expand Up @@ -1014,6 +1019,7 @@ impl App {
return;
}
self.loading = false;
self.clear_object_cache();
self.detail.target = None;
self.detail.log_target = None;
self.detail.exec_target = None;
Expand Down Expand Up @@ -1162,6 +1168,7 @@ impl App {
self.loading = false;
let count = objects.len();
self.objects = objects;
self.cache_current_objects();
self.set_object_status(count);
self.sync_status();
self.rebuild_object_list();
Expand All @@ -1173,7 +1180,14 @@ impl App {
}
self.loading = false;
self.stop_object_watch();
self.objects.clear();
if let Some(objects) = self
.current_object_cache_key()
.and_then(|key| self.cached_objects(&key))
{
self.objects = objects;
} else {
self.objects.clear();
}
self.status = tr("Unable to list selected resource.");
self.sync_status();
self.rebuild_object_list();
Expand All @@ -1186,14 +1200,17 @@ impl App {
match event {
ObjectWatchEvent::Restarted(objects) => {
self.objects = objects;
self.cache_current_objects();
self.schedule_object_list_refresh(&sender);
}
ObjectWatchEvent::Applied(object) => {
self.upsert_object(object);
self.cache_current_objects();
self.schedule_object_list_refresh(&sender);
}
ObjectWatchEvent::Deleted(object) => {
self.remove_object(&object);
self.cache_current_objects();
self.schedule_object_list_refresh(&sender);
}
ObjectWatchEvent::Error(error) => {
Expand Down
73 changes: 71 additions & 2 deletions crates/aetheris-app/src/app/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ impl App {
self.stop_object_watch();
self.stop_log_stream();
self.stop_port_forward();
self.object_cache.clear();
self.object_cache_order.clear();
self.detail.exec_target = None;
self.detail.port_forward_target = None;
self.loading = true;
Expand Down Expand Up @@ -275,9 +277,20 @@ impl App {
self.stop_object_watch();
self.object_load_token = self.object_load_token.saturating_add(1);
let token = self.object_load_token;
let cache_key = object_cache_key(&context, &resource, namespace.clone());
self.loading = true;
self.objects.clear();
self.status = tr_format("Loading {resource}...", &[("{resource}", resource.label())]);
if let Some(objects) = self.cached_objects(&cache_key) {
let count = objects.len();
self.objects = objects;
self.set_object_status(count);
self.status = tr_format(
"Refreshing {resource}...",
&[("{resource}", resource.label())],
);
} else {
self.objects.clear();
self.status = tr_format("Loading {resource}...", &[("{resource}", resource.label())]);
}
self.sync_status();
self.rebuild_object_list();
sender.oneshot_command(
Expand Down Expand Up @@ -682,6 +695,47 @@ impl App {
.splice(0, self.object_store.n_items(), &items);
}

pub(super) fn current_object_cache_key(&self) -> Option<ObjectCacheKey> {
let context = self.selected_context.as_ref()?;
let resource = self.selected_resource_kind()?;
let namespace = resource
.is_namespaced()
.then(|| self.selected_namespace.clone());
Some(object_cache_key(context, resource, namespace))
}

pub(super) fn cached_objects(&mut self, key: &ObjectCacheKey) -> Option<Vec<ObjectSummary>> {
let objects = self.object_cache.get(key)?.clone();
self.touch_object_cache_key(key.clone());
Some(objects)
}

pub(super) fn cache_current_objects(&mut self) {
if let Some(key) = self.current_object_cache_key() {
self.cache_objects(key, self.objects.clone());
}
}

pub(super) fn cache_objects(&mut self, key: ObjectCacheKey, objects: Vec<ObjectSummary>) {
self.object_cache.insert(key.clone(), objects);
self.touch_object_cache_key(key);
while self.object_cache_order.len() > OBJECT_CACHE_LIMIT {
if let Some(oldest) = self.object_cache_order.pop_front() {
self.object_cache.remove(&oldest);
}
}
}

pub(super) fn touch_object_cache_key(&mut self, key: ObjectCacheKey) {
self.object_cache_order.retain(|existing| existing != &key);
self.object_cache_order.push_back(key);
}

pub(super) fn clear_object_cache(&mut self) {
self.object_cache.clear();
self.object_cache_order.clear();
}

/// Merges one watch event into `objects` without sorting or repainting;
/// both are deferred to `flush_object_list_refresh` so an event burst
/// costs one refresh instead of thousands.
Expand Down Expand Up @@ -941,6 +995,21 @@ impl App {
}
}

fn object_cache_key(
context: &str,
resource: &ResourceKind,
namespace: Option<String>,
) -> ObjectCacheKey {
ObjectCacheKey {
context: context.to_owned(),
group: resource.group.clone(),
version: resource.version.clone(),
kind: resource.kind.clone(),
plural: resource.plural.clone(),
namespace,
}
}

/// The object at a view position, resolved against the sorted model the
/// `ColumnView` actually displays (positions differ from the backing store
/// whenever a header-click sort is active).
Expand Down
6 changes: 5 additions & 1 deletion crates/aetheris-app/src/app/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub(crate) enum ResourceSection {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum StatusFilter {
Ready,
Available,
Unavailable,
Running,
Pending,
Expand Down Expand Up @@ -198,8 +199,9 @@ pub(super) fn default_object_columns() -> Vec<ObjectColumn> {
const OBJECT_COLUMN_SCHEMA_VERSION: u32 = 1;

impl StatusFilter {
pub(super) const ALL: [Self; 5] = [
pub(super) const ALL: [Self; 6] = [
Self::Ready,
Self::Available,
Self::Unavailable,
Self::Running,
Self::Pending,
Expand All @@ -209,6 +211,7 @@ impl StatusFilter {
pub(super) fn label(self) -> String {
match self {
Self::Ready => tr("Ready"),
Self::Available => tr("Available"),
Self::Unavailable => tr("Unavailable"),
Self::Running => tr("Running"),
Self::Pending => tr("Pending"),
Expand Down Expand Up @@ -237,6 +240,7 @@ impl StatusFilter {
pub(super) fn keyword(self) -> &'static str {
match self {
Self::Ready => "Ready",
Self::Available => "Available",
Self::Unavailable => "Unavailable",
Self::Running => "Running",
Self::Pending => "Pending",
Expand Down
2 changes: 1 addition & 1 deletion crates/aetheris-app/src/app/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ fn filter_chip(

fn status_filter_tone(filter: StatusFilter) -> StatusTone {
match filter {
StatusFilter::Ready | StatusFilter::Running => StatusTone::Good,
StatusFilter::Ready | StatusFilter::Available | StatusFilter::Running => StatusTone::Good,
StatusFilter::Pending | StatusFilter::Unavailable => StatusTone::Warning,
StatusFilter::Failed => StatusTone::Bad,
}
Expand Down
Loading