From 3f698f59a94b812e335cd8e37b29e573ab610336 Mon Sep 17 00:00:00 2001 From: Deepu Date: Tue, 22 Aug 2023 09:05:11 +0200 Subject: [PATCH 1/3] move render and kube-api call to app structs --- Cargo.lock | 12 + Cargo.toml | 1 + src/app/configmaps.rs | 79 +- src/app/contexts.rs | 69 + src/app/cronjobs.rs | 96 +- src/app/daemonsets.rs | 98 +- src/app/deployments.rs | 92 +- src/app/dynamic.rs | 132 +- src/app/ingress.rs | 93 +- src/app/jobs.rs | 82 +- src/app/metrics.rs | 190 ++- src/app/models.rs | 11 +- src/app/network_policies.rs | 83 +- src/app/nodes.rs | 168 ++- src/app/ns.rs | 84 +- src/app/pods.rs | 226 +++- src/app/pvcs.rs | 102 +- src/app/pvs.rs | 102 +- src/app/replicasets.rs | 90 +- src/app/replication_controllers.rs | 104 +- src/app/roles.rs | 263 +++- src/app/secrets.rs | 85 +- src/app/serviceaccounts.rs | 87 +- src/app/statefulsets.rs | 83 +- src/app/storageclass.rs | 96 +- src/app/svcs.rs | 91 +- src/network/dynamic_resource.rs | 105 -- src/network/kube_api.rs | 431 ------- src/network/mod.rs | 228 +++- src/ui/contexts.rs | 54 - src/ui/mod.rs | 19 +- src/ui/overview.rs | 46 +- src/ui/resource_tabs.rs | 1899 +--------------------------- src/ui/utilization.rs | 108 -- src/ui/utils.rs | 301 ++++- 35 files changed, 3164 insertions(+), 2646 deletions(-) delete mode 100644 src/network/dynamic_resource.rs delete mode 100644 src/network/kube_api.rs delete mode 100644 src/ui/contexts.rs delete mode 100644 src/ui/utilization.rs diff --git a/Cargo.lock b/Cargo.lock index 1bddfc1c..49768abb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-trait" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -864,6 +875,7 @@ name = "kdash" version = "0.4.0" dependencies = [ "anyhow", + "async-trait", "backtrace", "base64 0.21.2", "cargo-husky", diff --git a/Cargo.toml b/Cargo.toml index 373176c5..db14a909 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ base64 ="0.21.2" openssl = { version = "0.10", features = ["vendored"] } human-panic = "1.1" kubectl-view-allocations = { version="0.16.3", default-features = false } +async-trait = "0.1.73" # XCB is a PITA to compile for ARM so disabling the copy feature on ARM for now [target.'cfg(target_arch = "x86_64")'.dependencies] diff --git a/src/app/configmaps.rs b/src/app/configmaps.rs index da760c13..21296ed2 100644 --- a/src/app/configmaps.rs +++ b/src/app/configmaps.rs @@ -1,8 +1,26 @@ use std::collections::BTreeMap; +use async_trait::async_trait; use k8s_openapi::{api::core::v1::ConfigMap, chrono::Utc}; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; -use super::{models::KubeResource, utils}; +use super::{ + models::{AppResource, KubeResource}, + utils, ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, DESCRIBE_AND_YAML_HINT, + }, +}; #[derive(Clone, PartialEq, Debug)] pub struct KubeConfigMap { @@ -31,6 +49,65 @@ impl KubeResource for KubeConfigMap { } } +static CONFIG_MAPS_TITLE: &str = "ConfigMaps"; + +pub struct ConfigMapResource {} + +#[async_trait] +impl AppResource for ConfigMapResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + CONFIG_MAPS_TITLE, + block, + f, + app, + area, + Self::render, + draw_block, + app.data.config_maps + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(ConfigMap::into).await; + + let mut app = nw.app.lock().await; + app.data.config_maps.set_items(items); + } +} + +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, CONFIG_MAPS_TITLE, "", app.data.config_maps.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_AND_YAML_HINT.into(), + resource: &mut app.data.config_maps, + table_headers: vec!["Namespace", "Name", "Data", "Age"], + column_widths: vec![ + Constraint::Percentage(30), + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(15), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.data.len().to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use k8s_openapi::chrono::Utc; diff --git a/src/app/contexts.rs b/src/app/contexts.rs index a6c2788a..7743b892 100644 --- a/src/app/contexts.rs +++ b/src/app/contexts.rs @@ -1,4 +1,24 @@ +use async_trait::async_trait; use kube::config::{Context, Kubeconfig, NamedContext}; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row, Table}, + Frame, +}; + +use crate::{ + network::Network, + ui::{ + utils::{ + layout_block_active, loading, style_highlight, style_primary, style_secondary, + table_header_style, + }, + HIGHLIGHT, + }, +}; + +use super::{models::AppResource, ActiveBlock, App}; #[derive(Clone, Default)] pub struct KubeContext { @@ -49,3 +69,52 @@ fn is_active_context( }, } } + +pub struct ContextResource {} + +#[async_trait] +impl AppResource for ContextResource { + fn render(_block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = format!(" Contexts [{}] ", app.data.contexts.items.len()); + let block = layout_block_active(title.as_str(), app.light_theme); + + if !app.data.contexts.items.is_empty() { + let rows = app.data.contexts.items.iter().map(|c| { + let style = if c.is_active { + style_secondary(app.light_theme) + } else { + style_primary(app.light_theme) + }; + Row::new(vec![ + Cell::from(c.name.as_ref()), + Cell::from(c.cluster.as_ref()), + Cell::from(c.user.as_ref()), + ]) + .style(style) + }); + + let table = Table::new(rows) + .header(table_header_style( + vec!["Context", "Cluster", "User"], + app.light_theme, + )) + .block(block) + .widths(&[ + Constraint::Percentage(34), + Constraint::Percentage(33), + Constraint::Percentage(33), + ]) + .highlight_style(style_highlight()) + .highlight_symbol(HIGHLIGHT); + + f.render_stateful_widget(table, area, &mut app.data.contexts.state); + } else { + loading(f, block, area, app.is_loading, app.light_theme); + } + } + + async fn get_resource(_nw: &Network<'_>) { + // not required + unimplemented!() + } +} diff --git a/src/app/cronjobs.rs b/src/app/cronjobs.rs index 0ce16950..139825de 100644 --- a/src/app/cronjobs.rs +++ b/src/app/cronjobs.rs @@ -1,6 +1,26 @@ use k8s_openapi::{api::batch::v1::CronJob, chrono::Utc}; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils, ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_YAML_AND_ESC_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubeCronJob { @@ -41,12 +61,86 @@ impl From for KubeCronJob { } } } + impl KubeResource for KubeCronJob { fn get_k8s_obj(&self) -> &CronJob { &self.k8s_obj } } +static CRON_JOBS_TITLE: &str = "CronJobs"; + +pub struct CronJobResource {} + +#[async_trait] +impl AppResource for CronJobResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + CRON_JOBS_TITLE, + block, + f, + app, + area, + Self::render, + draw_cronjobs_block, + app.data.cronjobs + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(CronJob::into).await; + + let mut app = nw.app.lock().await; + app.data.cronjobs.set_items(items); + } +} + +fn draw_cronjobs_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, CRON_JOBS_TITLE, "", app.data.cronjobs.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.cronjobs, + table_headers: vec![ + "Namespace", + "Name", + "Schedule", + "Last Scheduled", + "Suspend", + "Active", + "Age", + ], + column_widths: vec![ + Constraint::Percentage(20), + Constraint::Percentage(25), + Constraint::Percentage(15), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.schedule.to_owned()), + Cell::from(c.last_schedule.to_string()), + Cell::from(c.suspend.to_string()), + Cell::from(c.active.to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/daemonsets.rs b/src/app/daemonsets.rs index 286b41d1..0af78560 100644 --- a/src/app/daemonsets.rs +++ b/src/app/daemonsets.rs @@ -1,6 +1,25 @@ use k8s_openapi::{api::apps::v1::DaemonSet, chrono::Utc}; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils, ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, DESCRIBE_AND_YAML_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubeDaemonSet { @@ -40,12 +59,89 @@ impl From for KubeDaemonSet { } } } + impl KubeResource for KubeDaemonSet { fn get_k8s_obj(&self) -> &DaemonSet { &self.k8s_obj } } +static DAEMON_SETS_TITLE: &str = "DaemonSets"; + +pub struct DaemonSetResource {} + +#[async_trait] +impl AppResource for DaemonSetResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + DAEMON_SETS_TITLE, + block, + f, + app, + area, + Self::render, + draw_daemon_sets_block, + app.data.daemon_sets + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(DaemonSet::into).await; + + let mut app = nw.app.lock().await; + app.data.daemon_sets.set_items(items); + } +} + +fn draw_daemon_sets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, DAEMON_SETS_TITLE, "", app.data.daemon_sets.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_AND_YAML_HINT.into(), + resource: &mut app.data.daemon_sets, + table_headers: vec![ + "Namespace", + "Name", + "Desired", + "Current", + "Ready", + "Up-to-date", + "Available", + "Age", + ], + column_widths: vec![ + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.desired.to_string()), + Cell::from(c.current.to_string()), + Cell::from(c.ready.to_string()), + Cell::from(c.up_to_date.to_string()), + Cell::from(c.available.to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/deployments.rs b/src/app/deployments.rs index 8aab7d14..c30788e2 100644 --- a/src/app/deployments.rs +++ b/src/app/deployments.rs @@ -1,6 +1,25 @@ use k8s_openapi::{api::apps::v1::Deployment, chrono::Utc}; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils, ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, DESCRIBE_AND_YAML_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubeDeployment { @@ -39,12 +58,83 @@ impl From for KubeDeployment { } } } + impl KubeResource for KubeDeployment { fn get_k8s_obj(&self) -> &Deployment { &self.k8s_obj } } +static DEPLOYMENTS_TITLE: &str = "Deployments"; + +pub struct DeploymentResource {} + +#[async_trait] +impl AppResource for DeploymentResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + DEPLOYMENTS_TITLE, + block, + f, + app, + area, + Self::render, + draw_deployments_block, + app.data.deployments + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(Deployment::into).await; + + let mut app = nw.app.lock().await; + app.data.deployments.set_items(items); + } +} + +fn draw_deployments_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, DEPLOYMENTS_TITLE, "", app.data.deployments.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_AND_YAML_HINT.into(), + resource: &mut app.data.deployments, + table_headers: vec![ + "Namespace", + "Name", + "Ready", + "Up-to-date", + "Available", + "Age", + ], + column_widths: vec![ + Constraint::Percentage(25), + Constraint::Percentage(35), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.ready.to_owned()), + Cell::from(c.updated.to_string()), + Cell::from(c.available.to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/dynamic.rs b/src/app/dynamic.rs index 74bce181..393556a3 100644 --- a/src/app/dynamic.rs +++ b/src/app/dynamic.rs @@ -2,12 +2,31 @@ use k8s_openapi::chrono::Utc; use kube::{ core::DynamicObject, discovery::{ApiResource, Scope}, - ResourceExt, + Api, ResourceExt, }; -use crate::app::models::KubeResource; +use anyhow::anyhow; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; -use super::utils; +use super::{ + models::{AppResource, KubeResource}, + utils, ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_YAML_AND_ESC_HINT, + }, +}; #[derive(Clone, Debug)] pub struct KubeDynamicKind { @@ -59,6 +78,113 @@ impl KubeResource for KubeDynamicResource { } } +pub struct DynamicResource {} + +#[async_trait] +impl AppResource for DynamicResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = if let Some(res) = &app.data.selected.dynamic_kind { + res.kind.as_str() + } else { + "" + }; + draw_resource_tab!( + title, + block, + f, + app, + area, + Self::render, + draw_dynamic_res_block, + app.data.dynamic_resources + ); + } + + /// fetch entries for a custom resource from the cluster + async fn get_resource(nw: &Network<'_>) { + let mut app = nw.app.lock().await; + + if let Some(drs) = &app.data.selected.dynamic_kind { + let api: Api = if drs.scope == Scope::Cluster { + Api::all_with(nw.client.clone(), &drs.api_resource) + } else { + match &app.data.selected.ns { + Some(ns) => Api::namespaced_with(nw.client.clone(), ns, &drs.api_resource), + None => Api::all_with(nw.client.clone(), &drs.api_resource), + } + }; + + let items = match api.list(&Default::default()).await { + Ok(list) => list + .items + .iter() + .map(|item| KubeDynamicResource::from(item.clone())) + .collect::>(), + Err(e) => { + nw.handle_error(anyhow!("Failed to get dynamic resources. {:?}", e)) + .await; + return; + } + }; + app.data.dynamic_resources.set_items(items); + } + } +} + +fn draw_dynamic_res_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let (title, scope) = if let Some(res) = &app.data.selected.dynamic_kind { + (res.kind.as_str(), res.scope.clone()) + } else { + ("", Scope::Cluster) + }; + let title = get_resource_title(app, title, "", app.data.dynamic_resources.items.len()); + + let (table_headers, column_widths) = if scope == Scope::Cluster { + ( + vec!["Name", "Age"], + vec![Constraint::Percentage(70), Constraint::Percentage(30)], + ) + } else { + ( + vec!["Namespace", "Name", "Age"], + vec![ + Constraint::Percentage(30), + Constraint::Percentage(50), + Constraint::Percentage(20), + ], + ) + }; + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.dynamic_resources, + table_headers, + column_widths, + }, + |c| { + let rows = if scope == Scope::Cluster { + Row::new(vec![ + Cell::from(c.name.to_owned()), + Cell::from(c.age.to_owned()), + ]) + } else { + Row::new(vec![ + Cell::from(c.namespace.clone().unwrap_or_default()), + Cell::from(c.name.to_owned()), + Cell::from(c.age.to_owned()), + ]) + }; + rows.style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/ingress.rs b/src/app/ingress.rs index 5a60781a..7afbdae0 100644 --- a/src/app/ingress.rs +++ b/src/app/ingress.rs @@ -3,9 +3,27 @@ use k8s_openapi::{ chrono::Utc, }; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + use super::{ - models::KubeResource, + models::{AppResource, KubeResource}, utils::{self, UNKNOWN}, + ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_YAML_AND_ESC_HINT, + }, }; #[derive(Clone, Debug, PartialEq)] @@ -142,6 +160,79 @@ fn get_addresses(i_status: &Option) -> String { } } +static INGRESS_TITLE: &str = "Ingresses"; + +pub struct IngressResource {} + +#[async_trait] +impl AppResource for IngressResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + INGRESS_TITLE, + block, + f, + app, + area, + Self::render, + draw_ingress_block, + app.data.ingress + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(Ingress::into).await; + + let mut app = nw.app.lock().await; + app.data.ingress.set_items(items); + } +} + +fn draw_ingress_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, INGRESS_TITLE, "", app.data.ingress.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.ingress, + table_headers: vec![ + "Namespace", + "Name", + "Ingress class", + "Paths", + "Default backend", + "Addresses", + "Age", + ], + column_widths: vec![ + Constraint::Percentage(10), + Constraint::Percentage(20), + Constraint::Percentage(10), + Constraint::Percentage(25), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.ingress_class.to_owned()), + Cell::from(c.paths.to_owned()), + Cell::from(c.default_backend.to_owned()), + Cell::from(c.address.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/jobs.rs b/src/app/jobs.rs index 5f62eafb..64aa1c2c 100644 --- a/src/app/jobs.rs +++ b/src/app/jobs.rs @@ -1,6 +1,25 @@ use k8s_openapi::{api::batch::v1::Job, chrono::Utc}; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils, ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, DESCRIBE_AND_YAML_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubeJob { @@ -57,6 +76,67 @@ impl KubeResource for KubeJob { } } +static JOBS_TITLE: &str = "Jobs"; + +pub struct JobResource {} + +#[async_trait] +impl AppResource for JobResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + JOBS_TITLE, + block, + f, + app, + area, + Self::render, + draw_jobs_block, + app.data.jobs + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(Job::into).await; + + let mut app = nw.app.lock().await; + app.data.jobs.set_items(items); + } +} + +fn draw_jobs_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, JOBS_TITLE, "", app.data.jobs.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_AND_YAML_HINT.into(), + resource: &mut app.data.jobs, + table_headers: vec!["Namespace", "Name", "Completions", "Duration", "Age"], + column_widths: vec![ + Constraint::Percentage(25), + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.completions.to_owned()), + Cell::from(c.duration.to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/metrics.rs b/src/app/metrics.rs index dde95873..19b7f084 100644 --- a/src/app/metrics.rs +++ b/src/app/metrics.rs @@ -1,11 +1,33 @@ -// Based on https://github.com/davidB/kubectl-view-allocations - -use kube::api::ObjectMeta; -use kubectl_view_allocations::metrics::Usage; +use anyhow::anyhow; +use async_trait::async_trait; +use k8s_openapi::api::core::v1::{Node, Pod}; +use kube::{ + api::{ListParams, ObjectMeta}, + Api, +}; +use kubectl_view_allocations::{ + extract_allocatable_from_nodes, extract_allocatable_from_pods, + extract_utilizations_from_pod_metrics, make_qualifiers, metrics::PodMetrics, Resource, +}; +use kubectl_view_allocations::{metrics::Usage, qty::Qty, tree::provide_prefix}; use serde::{Deserialize, Serialize}; use tokio::sync::MutexGuard; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row, Table}, + Frame, +}; + +use crate::{ + network::Network, + ui::utils::{ + layout_block_active, loading, style_highlight, style_primary, style_success, style_warning, + table_header_style, + }, +}; -use super::{utils, App}; +use super::{models::AppResource, utils, ActiveBlock, App}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeMetrics { @@ -65,6 +87,164 @@ impl KubeNodeMetrics { } } +pub struct UtilizationResource {} + +#[async_trait] +impl AppResource for UtilizationResource { + fn render(_block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = format!( + " Resource Utilization (ns: [{}], group by : {:?}) ", + app + .data + .selected + .ns + .as_ref() + .unwrap_or(&String::from("all")), + app.utilization_group_by + ); + let block = layout_block_active(title.as_str(), app.light_theme); + + if !app.data.metrics.items.is_empty() { + let data = &app.data.metrics.items; + + let prefixes = provide_prefix(data, |parent, item| parent.0.len() + 1 == item.0.len()); + + // Create the table + let mut rows: Vec> = vec![]; + for ((k, oqtys), prefix) in data.iter().zip(prefixes.iter()) { + let column0 = format!( + "{} {}", + prefix, + k.last().map(|x| x.as_str()).unwrap_or("???") + ); + if let Some(qtys) = oqtys { + let style = if qtys.requested > qtys.limit || qtys.utilization > qtys.limit { + style_warning(app.light_theme) + } else if is_empty(&qtys.requested) || is_empty(&qtys.limit) { + style_primary(app.light_theme) + } else { + style_success(app.light_theme) + }; + + let row = Row::new(vec![ + Cell::from(column0), + make_table_cell(&qtys.utilization, &qtys.allocatable), + make_table_cell(&qtys.requested, &qtys.allocatable), + make_table_cell(&qtys.limit, &qtys.allocatable), + make_table_cell(&qtys.allocatable, &None), + make_table_cell(&qtys.calc_free(), &None), + ]) + .style(style); + rows.push(row); + } + } + + let table = Table::new(rows) + .header(table_header_style( + vec![ + "Resource", + "Utilization", + "Requested", + "Limit", + "Allocatable", + "Free", + ], + app.light_theme, + )) + .block(block) + .widths(&[ + Constraint::Percentage(50), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + ]) + .highlight_style(style_highlight()); + + f.render_stateful_widget(table, area, &mut app.data.metrics.state); + } else { + loading(f, block, area, app.is_loading, app.light_theme); + } + } + + async fn get_resource(nw: &Network<'_>) { + let mut resources: Vec = vec![]; + + let node_api: Api = Api::all(nw.client.clone()); + match node_api.list(&ListParams::default()).await { + Ok(node_list) => { + if let Err(e) = extract_allocatable_from_nodes(node_list, &mut resources).await { + nw.handle_error(anyhow!( + "Failed to extract node allocation metrics. {:?}", + e + )) + .await; + } + } + Err(e) => { + nw.handle_error(anyhow!( + "Failed to extract node allocation metrics. {:?}", + e + )) + .await + } + } + + let pod_api: Api = nw.get_namespaced_api().await; + match pod_api.list(&ListParams::default()).await { + Ok(pod_list) => { + if let Err(e) = extract_allocatable_from_pods(pod_list, &mut resources).await { + nw.handle_error(anyhow!("Failed to extract pod allocation metrics. {:?}", e)) + .await; + } + } + Err(e) => { + nw.handle_error(anyhow!("Failed to extract pod allocation metrics. {:?}", e)) + .await + } + } + + let api_pod_metrics: Api = Api::all(nw.client.clone()); + + match api_pod_metrics + .list(&ListParams::default()) + .await + { + Ok(pod_metrics) => { + if let Err(e) = extract_utilizations_from_pod_metrics(pod_metrics, &mut resources).await { + nw.handle_error(anyhow!("Failed to extract pod utilization metrics. {:?}", e)).await; + } + } + Err(_e) => nw.handle_error(anyhow!("Failed to extract pod utilization metrics. Make sure you have a metrics-server deployed on your cluster.")).await, + }; + + let mut app = nw.app.lock().await; + + let data = make_qualifiers(&resources, &app.utilization_group_by, &[]); + + app.data.metrics.set_items(data); + } +} + +fn make_table_cell<'a>(oqty: &Option, o100: &Option) -> Cell<'a> { + let txt = match oqty { + None => "__".into(), + Some(ref qty) => match o100 { + None => format!("{}", qty.adjust_scale()), + Some(q100) => format!("{} ({:.0}%)", qty.adjust_scale(), qty.calc_percentage(q100)), + }, + }; + Cell::from(txt) +} + +fn is_empty(oqty: &Option) -> bool { + match oqty { + Some(qty) => qty.is_zero(), + None => true, + } +} + #[cfg(test)] mod tests { use tokio::sync::Mutex; diff --git a/src/app/models.rs b/src/app/models.rs index 15a5942d..b3b2ed3c 100644 --- a/src/app/models.rs +++ b/src/app/models.rs @@ -1,5 +1,6 @@ use std::collections::VecDeque; +use async_trait::async_trait; use serde::Serialize; use tui::{ backend::Backend, @@ -10,8 +11,16 @@ use tui::{ Frame, }; -use super::Route; +use crate::network::Network; +use super::{ActiveBlock, App, Route}; + +#[async_trait] +pub trait AppResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect); + + async fn get_resource(network: &Network<'_>); +} pub trait KubeResource { fn get_k8s_obj(&self) -> &T; diff --git a/src/app/network_policies.rs b/src/app/network_policies.rs index 043e4c39..664476cb 100644 --- a/src/app/network_policies.rs +++ b/src/app/network_policies.rs @@ -2,7 +2,27 @@ use std::vec; use k8s_openapi::{api::networking::v1::NetworkPolicy, chrono::Utc}; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils, ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_YAML_AND_ESC_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubeNetworkPolicy { @@ -49,6 +69,67 @@ impl KubeResource for KubeNetworkPolicy { } } +static NW_POLICY_TITLE: &str = "NetworkPolicies"; + +pub struct NetworkPolicyResource {} + +#[async_trait] +impl AppResource for NetworkPolicyResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + NW_POLICY_TITLE, + block, + f, + app, + area, + Self::render, + draw_nw_policy_block, + app.data.nw_policies + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(NetworkPolicy::into).await; + + let mut app = nw.app.lock().await; + app.data.nw_policies.set_items(items); + } +} + +fn draw_nw_policy_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, NW_POLICY_TITLE, "", app.data.nw_policies.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.nw_policies, + table_headers: vec!["Namespace", "Name", "Pod Selector", "Policy Types", "Age"], + column_widths: vec![ + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(30), + Constraint::Percentage(20), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.pod_selector.to_owned()), + Cell::from(c.policy_types.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/nodes.rs b/src/app/nodes.rs index 0b74c3c9..5701081b 100644 --- a/src/app/nodes.rs +++ b/src/app/nodes.rs @@ -2,13 +2,35 @@ use k8s_openapi::{ api::core::v1::{Node, Pod}, chrono::Utc, }; -use kube::api::ObjectList; +use kube::{ + api::{ListParams, ObjectList}, + core::ListMeta, + Api, +}; use tokio::sync::MutexGuard; +use anyhow::anyhow; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + use super::{ - models::KubeResource, + metrics::{self, KubeNodeMetrics}, + models::{AppResource, KubeResource}, utils::{self, UNKNOWN}, - App, + ActiveBlock, App, +}; +use crate::{ + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_cluster_wide_resource_title, get_describe_active, + style_failure, style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_AND_YAML_HINT, + }, }; #[derive(Clone, Debug, PartialEq)] @@ -158,6 +180,146 @@ impl KubeResource for KubeNode { } } +static NODES_TITLE: &str = "Nodes"; + +pub struct NodeResource {} + +#[async_trait] +impl AppResource for NodeResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + match block { + ActiveBlock::Describe | ActiveBlock::Yaml => draw_describe_block( + f, + app, + area, + title_with_dual_style( + get_cluster_wide_resource_title( + NODES_TITLE, + app.data.nodes.items.len(), + get_describe_active(block), + ), + format!("{} | {} ", COPY_HINT, NODES_TITLE), + app.light_theme, + ), + ), + ActiveBlock::Namespaces => Self::render(app.get_prev_route().active_block, f, app, area), + _ => draw_nodes_block(f, app, area), + }; + } + + async fn get_resource(nw: &Network<'_>) { + let lp = ListParams::default(); + let api_pods: Api = Api::all(nw.client.clone()); + let api_nodes: Api = Api::all(nw.client.clone()); + + match api_nodes.list(&lp).await { + Ok(node_list) => { + get_node_metrics(nw).await; + + let pods_list = match api_pods.list(&lp).await { + Ok(list) => list, + Err(_) => ObjectList { + metadata: ListMeta::default(), + items: vec![], + }, + }; + + let mut app = nw.app.lock().await; + + let items = node_list + .iter() + .map(|node| KubeNode::from_api_with_pods(node, &pods_list, &mut app)) + .collect::>(); + + app.data.nodes.set_items(items); + } + Err(e) => { + nw.handle_error(anyhow!("Failed to get nodes. {:?}", e)) + .await; + } + } + } +} + +async fn get_node_metrics(nw: &Network<'_>) { + let api_node_metrics: Api = Api::all(nw.client.clone()); + + match api_node_metrics.list(&ListParams::default()).await { + Ok(node_metrics) => { + let mut app = nw.app.lock().await; + + let items = node_metrics + .iter() + .map(|metric| KubeNodeMetrics::from_api(metric, &app)) + .collect(); + + app.data.node_metrics = items; + } + Err(_) => { + let mut app = nw.app.lock().await; + app.data.node_metrics = vec![]; + // lets not show error as it will always be showing up and be annoying + // TODO may be show once and then disable polling + } + }; +} + +fn draw_nodes_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_cluster_wide_resource_title(NODES_TITLE, app.data.nodes.items.len(), ""); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_AND_YAML_HINT.into(), + resource: &mut app.data.nodes, + table_headers: vec![ + "Name", "Status", "Roles", "Version", "Pods", "CPU", "Mem", "CPU %", "Mem %", "CPU/A", + "Mem/A", "Age", + ], + column_widths: vec![ + Constraint::Percentage(25), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(5), + Constraint::Percentage(5), + Constraint::Percentage(5), + Constraint::Percentage(5), + Constraint::Percentage(5), + Constraint::Percentage(5), + Constraint::Percentage(5), + Constraint::Percentage(10), + ], + }, + |c| { + let style = if c.status != "Ready" { + style_failure(app.light_theme) + } else { + style_primary(app.light_theme) + }; + Row::new(vec![ + Cell::from(c.name.to_owned()), + Cell::from(c.status.to_owned()), + Cell::from(c.role.to_owned()), + Cell::from(c.version.to_owned()), + Cell::from(c.pods.to_string()), + Cell::from(c.cpu.to_owned()), + Cell::from(c.mem.to_owned()), + Cell::from(c.cpu_percent.to_owned()), + Cell::from(c.mem_percent.to_owned()), + Cell::from(c.cpu_a.to_owned()), + Cell::from(c.mem_a.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use tokio::sync::Mutex; diff --git a/src/app/ns.rs b/src/app/ns.rs index 7289b09d..797bf070 100644 --- a/src/app/ns.rs +++ b/src/app/ns.rs @@ -1,8 +1,30 @@ use k8s_openapi::api::core::v1::Namespace; +use anyhow::anyhow; +use async_trait::async_trait; +use kube::{api::ListParams, Api}; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row, Table}, + Frame, +}; + use super::{ - models::KubeResource, + key_binding::DEFAULT_KEYBINDING, + models::{AppResource, KubeResource}, utils::{self, UNKNOWN}, + ActiveBlock, App, +}; +use crate::{ + network::Network, + ui::{ + utils::{ + layout_block_default, loading, style_highlight, style_primary, style_secondary, + table_header_style, + }, + HIGHLIGHT, + }, }; #[derive(Clone, Debug, PartialEq, Default)] @@ -36,6 +58,66 @@ impl KubeResource for KubeNs { } } +pub struct NamespaceResource {} + +#[async_trait] +impl AppResource for NamespaceResource { + fn render(_block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = format!( + " Namespaces {} (all: {}) ", + DEFAULT_KEYBINDING.jump_to_namespace.key, DEFAULT_KEYBINDING.select_all_namespace.key + ); + let mut block = layout_block_default(title.as_str()); + + if app.get_current_route().active_block == ActiveBlock::Namespaces { + block = block.style(style_secondary(app.light_theme)) + } + + if !app.data.namespaces.items.is_empty() { + let rows = app.data.namespaces.items.iter().map(|s| { + let style = if Some(s.name.clone()) == app.data.selected.ns { + style_secondary(app.light_theme) + } else { + style_primary(app.light_theme) + }; + Row::new(vec![ + Cell::from(s.name.as_ref()), + Cell::from(s.status.as_ref()), + ]) + .style(style) + }); + + let table = Table::new(rows) + .header(table_header_style(vec!["Name", "Status"], app.light_theme)) + .block(block) + .highlight_style(style_highlight()) + .highlight_symbol(HIGHLIGHT) + .widths(&[Constraint::Length(22), Constraint::Length(6)]); + + f.render_stateful_widget(table, area, &mut app.data.namespaces.state); + } else { + loading(f, block, area, app.is_loading, app.light_theme); + } + } + + async fn get_resource(nw: &Network<'_>) { + let api: Api = Api::all(nw.client.clone()); + + let lp = ListParams::default(); + match api.list(&lp).await { + Ok(ns_list) => { + let items = ns_list.into_iter().map(KubeNs::from).collect::>(); + let mut app = nw.app.lock().await; + app.data.namespaces.set_items(items); + } + Err(e) => { + nw.handle_error(anyhow!("Failed to get namespaces. {:?}", e)) + .await; + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/pods.rs b/src/app/pods.rs index 044efaac..b35359bb 100644 --- a/src/app/pods.rs +++ b/src/app/pods.rs @@ -6,9 +6,27 @@ use k8s_openapi::{ chrono::Utc, }; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + style::Style, + widgets::{Cell, Row}, + Frame, +}; + use super::{ - models::KubeResource, + models::{AppResource, KubeResource}, utils::{self, UNKNOWN}, + ActiveBlock, App, +}; +use crate::{ + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + layout_block_top_border, loading, style_failure, style_primary, style_secondary, style_success, + title_with_dual_style, ResourceTableProps, COPY_HINT, DESCRIBE_AND_YAML_HINT, + }, }; #[derive(Clone, Default, Debug, PartialEq)] @@ -126,6 +144,203 @@ impl KubeResource for KubePod { } } +static PODS_TITLE: &str = "Pods"; +pub struct PodResource {} + +#[async_trait] +impl AppResource for PodResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + match block { + ActiveBlock::Containers => draw_containers_block(f, app, area), + ActiveBlock::Describe | ActiveBlock::Yaml => draw_describe_block( + f, + app, + area, + title_with_dual_style( + get_resource_title( + app, + PODS_TITLE, + get_describe_active(block), + app.data.pods.items.len(), + ), + format!("{} | {} ", COPY_HINT, PODS_TITLE), + app.light_theme, + ), + ), + ActiveBlock::Logs => draw_logs_block(f, app, area), + ActiveBlock::Namespaces => Self::render(app.get_prev_route().active_block, f, app, area), + _ => draw_pods_block(f, app, area), + } + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(Pod::into).await; + + let mut app = nw.app.lock().await; + if app.data.selected.pod.is_some() { + let containers = &items.iter().find_map(|pod| { + if pod.name == app.data.selected.pod.clone().unwrap() { + Some(&pod.containers) + } else { + None + } + }); + if containers.is_some() { + app.data.containers.set_items(containers.unwrap().clone()); + } + } + app.data.pods.set_items(items); + } +} + +fn draw_pods_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, PODS_TITLE, "", app.data.pods.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: format!("| Containers {}", DESCRIBE_AND_YAML_HINT), + resource: &mut app.data.pods, + table_headers: vec!["Namespace", "Name", "Ready", "Status", "Restarts", "Age"], + column_widths: vec![ + Constraint::Percentage(25), + Constraint::Percentage(35), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + let style = get_resource_row_style(c.status.as_str(), c.ready, app.light_theme); + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(format!("{}/{}", c.ready.0, c.ready.1)), + Cell::from(c.status.to_owned()), + Cell::from(c.restarts.to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style) + }, + app.light_theme, + app.is_loading, + ); +} + +fn draw_containers_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_container_title(app, app.data.containers.items.len(), ""); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: format!("| Logs | {} ", PODS_TITLE), + resource: &mut app.data.containers, + table_headers: vec![ + "Name", + "Image", + "Init", + "Ready", + "State", + "Restarts", + "Probes(L/R)", + "Ports", + "Age", + ], + column_widths: vec![ + Constraint::Percentage(20), + Constraint::Percentage(25), + Constraint::Percentage(5), + Constraint::Percentage(5), + Constraint::Percentage(10), + Constraint::Percentage(5), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + let style = get_resource_row_style(c.status.as_str(), (0, 0), app.light_theme); + Row::new(vec![ + Cell::from(c.name.to_owned()), + Cell::from(c.image.to_owned()), + Cell::from(c.init.to_string()), + Cell::from(c.ready.to_owned()), + Cell::from(c.status.to_owned()), + Cell::from(c.restarts.to_string()), + Cell::from(format!("{}/{}", c.liveliness_probe, c.readiness_probe,)), + Cell::from(c.ports.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style) + }, + app.light_theme, + app.is_loading, + ); +} + +fn get_container_title>(app: &App, container_len: usize, suffix: S) -> String { + let title = get_resource_title( + app, + PODS_TITLE, + format!("-> Containers [{}] {}", container_len, suffix.as_ref()).as_str(), + app.data.pods.items.len(), + ); + title +} + +fn draw_logs_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let selected_container = app.data.selected.container.clone(); + let container_name = selected_container.unwrap_or_default(); + + let title = title_with_dual_style( + get_container_title( + app, + app.data.containers.items.len(), + format!("-> Logs ({}) ", container_name), + ), + "| copy | Containers ".into(), + app.light_theme, + ); + + let block = layout_block_top_border(title); + + if container_name == app.data.logs.id { + app.data.logs.render_list( + f, + area, + block, + style_primary(app.light_theme), + app.log_auto_scroll, + ); + } else { + loading(f, block, area, app.is_loading, app.light_theme); + } +} + +fn get_resource_row_style(status: &str, ready: (i32, i32), light: bool) -> Style { + if status == "Running" && ready.0 == ready.1 { + style_primary(light) + } else if status == "Completed" { + style_success(light) + } else if [ + "ContainerCreating", + "PodInitializing", + "Pending", + "Initialized", + ] + .contains(&status) + { + style_secondary(light) + } else { + style_failure(light) + } +} + impl KubeContainer { pub fn from_api( container: &Container, @@ -326,6 +541,15 @@ mod tests { use super::*; use crate::app::test_utils::*; + #[test] + fn test_get_container_title() { + let app = App::default(); + assert_eq!( + get_container_title(&app, 3, "hello"), + " Pods (ns: all) [0] -> Containers [3] hello" + ); + } + #[test] fn test_pod_from_api() { let (pods, pods_list): (Vec, Vec<_>) = convert_resource_from_file("pods"); diff --git a/src/app/pvcs.rs b/src/app/pvcs.rs index f8525a7e..4c2156eb 100644 --- a/src/app/pvcs.rs +++ b/src/app/pvcs.rs @@ -2,7 +2,28 @@ use k8s_openapi::{ api::core::v1::PersistentVolumeClaim, apimachinery::pkg::api::resource::Quantity, chrono::Utc, }; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils::{self}, + ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_YAML_AND_ESC_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubePVC { @@ -69,6 +90,85 @@ impl KubeResource for KubePVC { } } +static PVC_TITLE: &str = "PersistentVolumeClaims"; + +pub struct PvcResource {} + +#[async_trait] +impl AppResource for PvcResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + PVC_TITLE, + block, + f, + app, + area, + Self::render, + draw_pvc_block, + app.data.pvcs + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw + .get_namespaced_resources(PersistentVolumeClaim::into) + .await; + + let mut app = nw.app.lock().await; + app.data.pvcs.set_items(items); + } +} + +fn draw_pvc_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, PVC_TITLE, "", app.data.pvcs.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.pvcs, + table_headers: vec![ + "Namespace", + "Name", + "Status", + "Volume", + "Capacity", + "Access Modes", + "Storage Class", + "Age", + ], + column_widths: vec![ + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(20), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.status.to_owned()), + Cell::from(c.volume.to_owned()), + Cell::from(c.capacity.to_owned()), + Cell::from(c.access_modes.to_owned()), + Cell::from(c.storage_class.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/pvs.rs b/src/app/pvs.rs index a8bef0cb..616ea736 100644 --- a/src/app/pvs.rs +++ b/src/app/pvs.rs @@ -2,7 +2,28 @@ use k8s_openapi::{ api::core::v1::PersistentVolume, apimachinery::pkg::api::resource::Quantity, chrono::Utc, }; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils::{self}, + ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_YAML_AND_ESC_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubePV { @@ -88,6 +109,85 @@ impl KubeResource for KubePV { } } +static PV_TITLE: &str = "PersistentVolumes"; + +pub struct PvResource {} + +#[async_trait] +impl AppResource for PvResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + PV_TITLE, + block, + f, + app, + area, + Self::render, + draw_pv_block, + app.data.pvs + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_resources(PersistentVolume::into).await; + + let mut app = nw.app.lock().await; + app.data.pvs.set_items(items); + } +} + +fn draw_pv_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, PV_TITLE, "", app.data.pvs.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.pvs, + table_headers: vec![ + "Name", + "Capacity", + "Access Modes", + "Reclaim Policy", + "Status", + "Claim", + "Storage Class", + "Reason", + "Age", + ], + column_widths: vec![ + Constraint::Percentage(20), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.name.to_owned()), + Cell::from(c.capacity.to_owned()), + Cell::from(c.access_modes.to_owned()), + Cell::from(c.reclaim_policy.to_owned()), + Cell::from(c.status.to_owned()), + Cell::from(c.claim.to_owned()), + Cell::from(c.storage_class.to_owned()), + Cell::from(c.reason.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/replicasets.rs b/src/app/replicasets.rs index d37a78bb..fac49e49 100644 --- a/src/app/replicasets.rs +++ b/src/app/replicasets.rs @@ -1,6 +1,26 @@ use k8s_openapi::{api::apps::v1::ReplicaSet, chrono::Utc}; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils::{self}, + ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, DESCRIBE_AND_YAML_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubeReplicaSet { @@ -41,6 +61,74 @@ impl KubeResource for KubeReplicaSet { } } +static REPLICA_SETS_TITLE: &str = "ReplicaSets"; + +pub struct ReplicaSetResource {} + +#[async_trait] +impl AppResource for ReplicaSetResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + REPLICA_SETS_TITLE, + block, + f, + app, + area, + Self::render, + draw_replica_sets_block, + app.data.replica_sets + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(ReplicaSet::into).await; + + let mut app = nw.app.lock().await; + app.data.replica_sets.set_items(items); + } +} + +fn draw_replica_sets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title( + app, + REPLICA_SETS_TITLE, + "", + app.data.replica_sets.items.len(), + ); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_AND_YAML_HINT.into(), + resource: &mut app.data.replica_sets, + table_headers: vec!["Namespace", "Name", "Desired", "Current", "Ready", "Age"], + column_widths: vec![ + Constraint::Percentage(25), + Constraint::Percentage(35), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.desired.to_string()), + Cell::from(c.current.to_string()), + Cell::from(c.ready.to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/replication_controllers.rs b/src/app/replication_controllers.rs index 24465854..f9f2b285 100644 --- a/src/app/replication_controllers.rs +++ b/src/app/replication_controllers.rs @@ -2,7 +2,28 @@ use std::collections::BTreeMap; use k8s_openapi::{api::core::v1::ReplicationController, chrono::Utc}; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils::{self}, + ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_YAML_AND_ESC_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubeReplicationController { @@ -81,6 +102,87 @@ impl KubeResource for KubeReplicationController { } } +static RPL_CTRL_TITLE: &str = "ReplicationControllers"; + +pub struct ReplicationControllerResource {} + +#[async_trait] +impl AppResource for ReplicationControllerResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + RPL_CTRL_TITLE, + block, + f, + app, + area, + Self::render, + draw_replication_controllers_block, + app.data.rpl_ctrls + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw + .get_namespaced_resources(ReplicationController::into) + .await; + + let mut app = nw.app.lock().await; + app.data.rpl_ctrls.set_items(items); + } +} + +fn draw_replication_controllers_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, RPL_CTRL_TITLE, "", app.data.rpl_ctrls.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.rpl_ctrls, + table_headers: vec![ + "Namespace", + "Name", + "Desired", + "Current", + "Ready", + "Containers", + "Images", + "Selector", + "Age", + ], + column_widths: vec![ + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.desired.to_string()), + Cell::from(c.current.to_string()), + Cell::from(c.ready.to_string()), + Cell::from(c.containers.to_owned()), + Cell::from(c.images.to_owned()), + Cell::from(c.selector.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/roles.rs b/src/app/roles.rs index d84e4d8d..0f661420 100644 --- a/src/app/roles.rs +++ b/src/app/roles.rs @@ -3,7 +3,28 @@ use k8s_openapi::{ chrono::Utc, }; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils::{self}, + ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_YAML_AND_ESC_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubeRole { @@ -54,6 +75,63 @@ impl KubeResource for KubeRole { } } +static ROLES_TITLE: &str = "Roles"; + +pub struct RoleResource {} + +#[async_trait] +impl AppResource for RoleResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + ROLES_TITLE, + block, + f, + app, + area, + Self::render, + draw_roles_block, + app.data.roles + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(Role::into).await; + + let mut app = nw.app.lock().await; + app.data.roles.set_items(items); + } +} + +fn draw_roles_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, ROLES_TITLE, "", app.data.roles.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.roles, + table_headers: vec!["Namespace", "Name", "Age"], + column_widths: vec![ + Constraint::Percentage(40), + Constraint::Percentage(40), + Constraint::Percentage(20), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + impl From for KubeClusterRole { fn from(cluster_role: ClusterRole) -> Self { KubeClusterRole { @@ -73,6 +151,63 @@ impl KubeResource for KubeClusterRole { } } +static CLUSTER_ROLES_TITLE: &str = "ClusterRoles"; + +pub struct ClusterRoleResource {} + +#[async_trait] +impl AppResource for ClusterRoleResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + CLUSTER_ROLES_TITLE, + block, + f, + app, + area, + Self::render, + draw_cluster_roles_block, + app.data.cluster_roles + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_resources(ClusterRole::into).await; + + let mut app = nw.app.lock().await; + app.data.cluster_roles.set_items(items); + } +} + +fn draw_cluster_roles_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title( + app, + CLUSTER_ROLES_TITLE, + "", + app.data.cluster_roles.items.len(), + ); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.cluster_roles, + table_headers: vec!["Name", "Age"], + column_widths: vec![Constraint::Percentage(50), Constraint::Percentage(50)], + }, + |c| { + Row::new(vec![ + Cell::from(c.name.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + impl From for KubeRoleBinding { fn from(role_binding: RoleBinding) -> Self { KubeRoleBinding { @@ -93,6 +228,70 @@ impl KubeResource for KubeRoleBinding { } } +static ROLE_BINDINGS_TITLE: &str = "RoleBindings"; + +pub struct RoleBindingResource {} + +#[async_trait] +impl AppResource for RoleBindingResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + ROLE_BINDINGS_TITLE, + block, + f, + app, + area, + Self::render, + draw_role_bindings_block, + app.data.role_bindings + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(RoleBinding::into).await; + + let mut app = nw.app.lock().await; + app.data.role_bindings.set_items(items); + } +} + +fn draw_role_bindings_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title( + app, + ROLE_BINDINGS_TITLE, + "", + app.data.role_bindings.items.len(), + ); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.role_bindings, + table_headers: vec!["Namespace", "Name", "Role", "Age"], + column_widths: vec![ + Constraint::Percentage(20), + Constraint::Percentage(30), + Constraint::Percentage(30), + Constraint::Percentage(20), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.role.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + impl From for KubeClusterRoleBinding { fn from(crb: ClusterRoleBinding) -> Self { KubeClusterRoleBinding { @@ -110,6 +309,68 @@ impl KubeResource for KubeClusterRoleBinding { } } +static CLUSTER_ROLES_BINDING_TITLE: &str = "ClusterRoleBinding"; + +pub struct ClusterRoleBindingResource {} + +#[async_trait] +impl AppResource for ClusterRoleBindingResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + CLUSTER_ROLES_BINDING_TITLE, + block, + f, + app, + area, + Self::render, + draw_cluster_role_binding_block, + app.data.cluster_role_bindings + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_resources(ClusterRoleBinding::into).await; + + let mut app = nw.app.lock().await; + app.data.cluster_role_bindings.set_items(items); + } +} + +fn draw_cluster_role_binding_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title( + app, + CLUSTER_ROLES_BINDING_TITLE, + "", + app.data.cluster_role_bindings.items.len(), + ); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.cluster_role_bindings, + table_headers: vec!["Name", "Role", "Age"], + column_widths: vec![ + Constraint::Percentage(40), + Constraint::Percentage(40), + Constraint::Percentage(20), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.name.to_owned()), + Cell::from(c.role.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use k8s_openapi::chrono::Utc; diff --git a/src/app/secrets.rs b/src/app/secrets.rs index 15eef262..3893774d 100644 --- a/src/app/secrets.rs +++ b/src/app/secrets.rs @@ -2,7 +2,28 @@ use std::collections::BTreeMap; use k8s_openapi::{api::core::v1::Secret, chrono::Utc, ByteString}; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils::{self}, + ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_YAML_DECODE_AND_ESC_HINT, + }, +}; #[derive(Clone, Debug, Default, PartialEq)] pub struct KubeSecret { @@ -26,12 +47,74 @@ impl From for KubeSecret { } } } + impl KubeResource for KubeSecret { fn get_k8s_obj(&self) -> &Secret { &self.k8s_obj } } +static SECRETS_TITLE: &str = "Secrets"; + +pub struct SecretResource {} + +#[async_trait] +impl AppResource for SecretResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + SECRETS_TITLE, + block, + f, + app, + area, + Self::render, + draw_secrets_block, + app.data.secrets + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(Secret::into).await; + + let mut app = nw.app.lock().await; + app.data.secrets.set_items(items); + } +} + +fn draw_secrets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, SECRETS_TITLE, "", app.data.secrets.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_DECODE_AND_ESC_HINT.into(), + resource: &mut app.data.secrets, + table_headers: vec!["Namespace", "Name", "Type", "Data", "Age"], + column_widths: vec![ + Constraint::Percentage(25), + Constraint::Percentage(30), + Constraint::Percentage(25), + Constraint::Percentage(10), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.type_.to_owned()), + Cell::from(c.data.len().to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use k8s_openapi::chrono::Utc; diff --git a/src/app/serviceaccounts.rs b/src/app/serviceaccounts.rs index c6efd1ae..531ab4ef 100644 --- a/src/app/serviceaccounts.rs +++ b/src/app/serviceaccounts.rs @@ -1,6 +1,27 @@ use k8s_openapi::{api::core::v1::ServiceAccount, chrono::Utc}; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils::{self}, + ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_YAML_AND_ESC_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubeSvcAcct { @@ -30,6 +51,70 @@ impl KubeResource for KubeSvcAcct { } } +static SVC_ACCT_TITLE: &str = "ServiceAccounts"; + +pub struct SvcAcctResource {} + +#[async_trait] +impl AppResource for SvcAcctResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + SVC_ACCT_TITLE, + block, + f, + app, + area, + Self::render, + draw_svc_acct_block, + app.data.service_accounts + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(ServiceAccount::into).await; + + let mut app = nw.app.lock().await; + app.data.service_accounts.set_items(items); + } +} + +fn draw_svc_acct_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title( + app, + SVC_ACCT_TITLE, + "", + app.data.service_accounts.items.len(), + ); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.service_accounts, + table_headers: vec!["Namespace", "Name", "Secrets", "Age"], + column_widths: vec![ + Constraint::Percentage(30), + Constraint::Percentage(30), + Constraint::Percentage(20), + Constraint::Percentage(20), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.secrets.to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use k8s_openapi::chrono::Utc; diff --git a/src/app/statefulsets.rs b/src/app/statefulsets.rs index d90d5e90..ec7a484d 100644 --- a/src/app/statefulsets.rs +++ b/src/app/statefulsets.rs @@ -1,6 +1,26 @@ use k8s_openapi::{api::apps::v1::StatefulSet, chrono::Utc}; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils::{self}, + ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, DESCRIBE_AND_YAML_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubeStatefulSet { @@ -39,6 +59,67 @@ impl KubeResource for KubeStatefulSet { } } +static STFS_TITLE: &str = "StatefulSets"; + +pub struct StatefulSetResource {} + +#[async_trait] +impl AppResource for StatefulSetResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + STFS_TITLE, + block, + f, + app, + area, + Self::render, + draw_stateful_sets_block, + app.data.stateful_sets + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(StatefulSet::into).await; + + let mut app = nw.app.lock().await; + app.data.stateful_sets.set_items(items); + } +} + +fn draw_stateful_sets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, STFS_TITLE, "", app.data.stateful_sets.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_AND_YAML_HINT.into(), + resource: &mut app.data.stateful_sets, + table_headers: vec!["Namespace", "Name", "Ready", "Service", "Age"], + column_widths: vec![ + Constraint::Percentage(25), + Constraint::Percentage(30), + Constraint::Percentage(10), + Constraint::Percentage(25), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.ready.to_owned()), + Cell::from(c.service.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/app/storageclass.rs b/src/app/storageclass.rs index 7b752f0f..9ffcef41 100644 --- a/src/app/storageclass.rs +++ b/src/app/storageclass.rs @@ -1,6 +1,26 @@ use k8s_openapi::{api::storage::v1::StorageClass, chrono::Utc}; -use super::{models::KubeResource, utils}; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + +use super::{ + models::{AppResource, KubeResource}, + utils::{self}, + ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_cluster_wide_resource_title, get_describe_active, + get_resource_title, style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, DESCRIBE_YAML_AND_ESC_HINT, + }, +}; #[derive(Clone, Debug, PartialEq)] pub struct KubeStorageClass { @@ -39,6 +59,80 @@ impl KubeResource for KubeStorageClass { } } +static STORAGE_CLASSES_LABEL: &str = "StorageClasses"; + +pub struct StorageClassResource {} + +#[async_trait] +impl AppResource for StorageClassResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + STORAGE_CLASSES_LABEL, + block, + f, + app, + area, + Self::render, + draw_storage_classes_block, + app.data.storage_classes + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_resources(StorageClass::into).await; + + let mut app = nw.app.lock().await; + app.data.storage_classes.set_items(items); + } +} + +fn draw_storage_classes_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_cluster_wide_resource_title( + STORAGE_CLASSES_LABEL, + app.data.storage_classes.items.len(), + "", + ); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.storage_classes, + table_headers: vec![ + "Name", + "Provisioner", + "Reclaim Policy", + "Volume Binding Mode", + "Allow Volume Expansion", + "Age", + ], + column_widths: vec![ + Constraint::Percentage(10), + Constraint::Percentage(20), + Constraint::Percentage(10), + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.name.to_owned()), + Cell::from(c.provisioner.to_owned()), + Cell::from(c.reclaim_policy.to_owned()), + Cell::from(c.volume_binding_mode.to_owned()), + Cell::from(c.allow_volume_expansion.to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + #[cfg(test)] mod tests { use k8s_openapi::chrono::Utc; diff --git a/src/app/svcs.rs b/src/app/svcs.rs index a9396b40..a326799e 100644 --- a/src/app/svcs.rs +++ b/src/app/svcs.rs @@ -3,9 +3,26 @@ use k8s_openapi::{ chrono::Utc, }; +use async_trait::async_trait; +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + widgets::{Cell, Row}, + Frame, +}; + use super::{ - models::KubeResource, + models::{AppResource, KubeResource}, utils::{self, UNKNOWN}, + ActiveBlock, App, +}; +use crate::{ + draw_resource_tab, + network::Network, + ui::utils::{ + draw_describe_block, draw_resource_block, get_describe_active, get_resource_title, + style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, DESCRIBE_AND_YAML_HINT, + }, }; #[derive(Clone, Debug, PartialEq)] @@ -70,6 +87,78 @@ impl KubeResource for KubeSvc { } } +static SERVICES_TITLE: &str = "Services"; + +pub struct SvcResource {} + +#[async_trait] +impl AppResource for SvcResource { + fn render(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + draw_resource_tab!( + SERVICES_TITLE, + block, + f, + app, + area, + Self::render, + draw_services_block, + app.data.services + ); + } + + async fn get_resource(nw: &Network<'_>) { + let items: Vec = nw.get_namespaced_resources(Service::into).await; + let mut app = nw.app.lock().await; + app.data.services.set_items(items); + } +} + +fn draw_services_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = get_resource_title(app, SERVICES_TITLE, "", app.data.services.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_AND_YAML_HINT.into(), + resource: &mut app.data.services, + table_headers: vec![ + "Namespace", + "Name", + "Type", + "Cluster IP", + "External IP", + "Ports", + "Age", + ], + column_widths: vec![ + Constraint::Percentage(10), + Constraint::Percentage(25), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(15), + Constraint::Percentage(20), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.type_.to_owned()), + Cell::from(c.cluster_ip.to_owned()), + Cell::from(c.external_ip.to_owned()), + Cell::from(c.ports.to_owned()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + fn get_ports(s_ports: &Option>) -> Option { s_ports.as_ref().map(|ports| { ports diff --git a/src/network/dynamic_resource.rs b/src/network/dynamic_resource.rs deleted file mode 100644 index 1b300db4..00000000 --- a/src/network/dynamic_resource.rs +++ /dev/null @@ -1,105 +0,0 @@ -use anyhow::anyhow; -use kube::{ - core::DynamicObject, - discovery::{verbs, Scope}, - Api, Discovery, -}; - -use crate::app::{ - dynamic::{KubeDynamicKind, KubeDynamicResource}, - models::StatefulList, - ActiveBlock, -}; - -use super::Network; - -impl<'a> Network<'a> { - /// Discover and cache custom resources on the cluster - pub async fn discover_dynamic_resources(&self) { - let discovery = match Discovery::new(self.client.clone()).run().await { - Ok(d) => d, - Err(e) => { - self - .handle_error(anyhow!("Failed to get dynamic resources. {:?}", e)) - .await; - return; - } - }; - - let mut dynamic_resources = vec![]; - let mut dynamic_menu = vec![]; - - let excluded = vec![ - "Namespace", - "Pod", - "Service", - "Node", - "ConfigMap", - "StatefulSet", - "ReplicaSet", - "Deployment", - "Job", - "DaemonSet", - "CronJob", - "Secret", - "ReplicationController", - "PersistentVolumeClaim", - "PersistentVolume", - "StorageClass", - "Role", - "RoleBinding", - "ClusterRole", - "ClusterRoleBinding", - "ServiceAccount", - "Ingress", - "NetworkPolicy", - ]; - - for group in discovery.groups() { - for (ar, caps) in group.recommended_resources() { - if !caps.supports_operation(verbs::LIST) || excluded.contains(&ar.kind.as_str()) { - continue; - } - - dynamic_menu.push((ar.kind.to_string(), ActiveBlock::DynamicResource)); - dynamic_resources.push(KubeDynamicKind::new(ar, caps.scope)); - } - } - let mut app = self.app.lock().await; - // sort dynamic_menu alphabetically using the first element of the tuple - dynamic_menu.sort_by(|a, b| a.0.cmp(&b.0)); - app.dynamic_resources_menu = StatefulList::with_items(dynamic_menu); - app.data.dynamic_kinds = dynamic_resources.clone(); - } - - /// fetch entries for a custom resource from the cluster - pub async fn get_dynamic_resources(&self) { - let mut app = self.app.lock().await; - - if let Some(drs) = &app.data.selected.dynamic_kind { - let api: Api = if drs.scope == Scope::Cluster { - Api::all_with(self.client.clone(), &drs.api_resource) - } else { - match &app.data.selected.ns { - Some(ns) => Api::namespaced_with(self.client.clone(), ns, &drs.api_resource), - None => Api::all_with(self.client.clone(), &drs.api_resource), - } - }; - - let items = match api.list(&Default::default()).await { - Ok(list) => list - .items - .iter() - .map(|item| KubeDynamicResource::from(item.clone())) - .collect::>(), - Err(e) => { - self - .handle_error(anyhow!("Failed to get dynamic resources. {:?}", e)) - .await; - return; - } - }; - app.data.dynamic_resources.set_items(items); - } - } -} diff --git a/src/network/kube_api.rs b/src/network/kube_api.rs deleted file mode 100644 index fce64591..00000000 --- a/src/network/kube_api.rs +++ /dev/null @@ -1,431 +0,0 @@ -use std::fmt; - -use anyhow::anyhow; -use k8s_openapi::{ - api::{ - apps::v1::{DaemonSet, Deployment, ReplicaSet, StatefulSet}, - batch::v1::{CronJob, Job}, - core::v1::{ - ConfigMap, Namespace, Node, PersistentVolume, PersistentVolumeClaim, Pod, - ReplicationController, Secret, Service, ServiceAccount, - }, - networking::v1::{Ingress, NetworkPolicy}, - rbac::v1::{ClusterRole, ClusterRoleBinding, Role, RoleBinding}, - storage::v1::StorageClass, - }, - NamespaceResourceScope, -}; -use kube::{ - api::{ListMeta, ListParams, ObjectList}, - config::Kubeconfig, - Api, Resource as ApiResource, -}; -use kubectl_view_allocations::{ - extract_allocatable_from_nodes, extract_allocatable_from_pods, - extract_utilizations_from_pod_metrics, make_qualifiers, metrics::PodMetrics, Resource, -}; -use serde::de::DeserializeOwned; - -use super::Network; -use crate::app::{ - configmaps::KubeConfigMap, - contexts, - cronjobs::KubeCronJob, - daemonsets::KubeDaemonSet, - deployments::KubeDeployment, - ingress::KubeIngress, - jobs::KubeJob, - metrics::{self, KubeNodeMetrics}, - network_policies::KubeNetworkPolicy, - nodes::KubeNode, - ns::KubeNs, - pods::KubePod, - pvcs::KubePVC, - pvs::KubePV, - replicasets::KubeReplicaSet, - replication_controllers::KubeReplicationController, - roles::{KubeClusterRole, KubeClusterRoleBinding, KubeRole, KubeRoleBinding}, - secrets::KubeSecret, - serviceaccounts::KubeSvcAcct, - statefulsets::KubeStatefulSet, - storageclass::KubeStorageClass, - svcs::KubeSvc, -}; - -impl<'a> Network<'a> { - pub async fn get_kube_config(&self) { - match Kubeconfig::read() { - Ok(config) => { - let mut app = self.app.lock().await; - let selected_ctx = app.data.selected.context.to_owned(); - app.set_contexts(contexts::get_contexts(&config, selected_ctx)); - app.data.kubeconfig = Some(config); - } - Err(e) => { - self - .handle_error(anyhow!("Failed to load Kubernetes config. {:?}", e)) - .await; - } - } - } - - pub async fn get_node_metrics(&self) { - let api_node_metrics: Api = Api::all(self.client.clone()); - - match api_node_metrics.list(&ListParams::default()).await { - Ok(node_metrics) => { - let mut app = self.app.lock().await; - - let items = node_metrics - .iter() - .map(|metric| KubeNodeMetrics::from_api(metric, &app)) - .collect(); - - app.data.node_metrics = items; - } - Err(_) => { - let mut app = self.app.lock().await; - app.data.node_metrics = vec![]; - // lets not show error as it will always be showing up and be annoying - // TODO may be show once and then disable polling - } - }; - } - - pub async fn get_utilizations(&self) { - let mut resources: Vec = vec![]; - - let api: Api = Api::all(self.client.clone()); - match api.list(&ListParams::default()).await { - Ok(node_list) => { - if let Err(e) = extract_allocatable_from_nodes(node_list, &mut resources).await { - self - .handle_error(anyhow!( - "Failed to extract node allocation metrics. {:?}", - e - )) - .await; - } - } - Err(e) => { - self - .handle_error(anyhow!( - "Failed to extract node allocation metrics. {:?}", - e - )) - .await - } - } - - let api: Api = self.get_namespaced_api().await; - match api.list(&ListParams::default()).await { - Ok(pod_list) => { - if let Err(e) = extract_allocatable_from_pods(pod_list, &mut resources).await { - self - .handle_error(anyhow!("Failed to extract pod allocation metrics. {:?}", e)) - .await; - } - } - Err(e) => { - self - .handle_error(anyhow!("Failed to extract pod allocation metrics. {:?}", e)) - .await - } - } - - let api_pod_metrics: Api = Api::all(self.client.clone()); - - match api_pod_metrics - .list(&ListParams::default()) - .await - { - Ok(pod_metrics) => { - if let Err(e) = extract_utilizations_from_pod_metrics(pod_metrics, &mut resources).await { - self.handle_error(anyhow!("Failed to extract pod utilization metrics. {:?}", e)).await; - } - } - Err(_e) => self.handle_error(anyhow!("Failed to extract pod utilization metrics. Make sure you have a metrics-server deployed on your cluster.")).await, - }; - - let mut app = self.app.lock().await; - - let data = make_qualifiers(&resources, &app.utilization_group_by, &[]); - - app.data.metrics.set_items(data); - } - - pub async fn get_nodes(&self) { - let lp = ListParams::default(); - let api_pods: Api = Api::all(self.client.clone()); - let api_nodes: Api = Api::all(self.client.clone()); - - match api_nodes.list(&lp).await { - Ok(node_list) => { - self.get_node_metrics().await; - - let pods_list = match api_pods.list(&lp).await { - Ok(list) => list, - Err(_) => ObjectList { - metadata: ListMeta::default(), - items: vec![], - }, - }; - - let mut app = self.app.lock().await; - - let items = node_list - .iter() - .map(|node| KubeNode::from_api_with_pods(node, &pods_list, &mut app)) - .collect::>(); - - app.data.nodes.set_items(items); - } - Err(e) => { - self - .handle_error(anyhow!("Failed to get nodes. {:?}", e)) - .await; - } - } - } - - pub async fn get_namespaces(&self) { - let api: Api = Api::all(self.client.clone()); - - let lp = ListParams::default(); - match api.list(&lp).await { - Ok(ns_list) => { - let items = ns_list.into_iter().map(KubeNs::from).collect::>(); - let mut app = self.app.lock().await; - app.data.namespaces.set_items(items); - } - Err(e) => { - self - .handle_error(anyhow!("Failed to get namespaces. {:?}", e)) - .await; - } - } - } - - pub async fn get_pods(&self) { - let items: Vec = self.get_namespaced_resources(Pod::into).await; - - let mut app = self.app.lock().await; - if app.data.selected.pod.is_some() { - let containers = &items.iter().find_map(|pod| { - if pod.name == app.data.selected.pod.clone().unwrap() { - Some(&pod.containers) - } else { - None - } - }); - if containers.is_some() { - app.data.containers.set_items(containers.unwrap().clone()); - } - } - app.data.pods.set_items(items); - } - - pub async fn get_services(&self) { - let items: Vec = self.get_namespaced_resources(Service::into).await; - let mut app = self.app.lock().await; - app.data.services.set_items(items); - } - - pub async fn get_config_maps(&self) { - let items: Vec = self.get_namespaced_resources(ConfigMap::into).await; - - let mut app = self.app.lock().await; - app.data.config_maps.set_items(items); - } - - pub async fn get_stateful_sets(&self) { - let items: Vec = self.get_namespaced_resources(StatefulSet::into).await; - - let mut app = self.app.lock().await; - app.data.stateful_sets.set_items(items); - } - - pub async fn get_replica_sets(&self) { - let items: Vec = self.get_namespaced_resources(ReplicaSet::into).await; - - let mut app = self.app.lock().await; - app.data.replica_sets.set_items(items); - } - - pub async fn get_jobs(&self) { - let items: Vec = self.get_namespaced_resources(Job::into).await; - - let mut app = self.app.lock().await; - app.data.jobs.set_items(items); - } - - pub async fn get_cron_jobs(&self) { - let items: Vec = self.get_namespaced_resources(CronJob::into).await; - - let mut app = self.app.lock().await; - app.data.cronjobs.set_items(items); - } - - pub async fn get_secrets(&self) { - let items: Vec = self.get_namespaced_resources(Secret::into).await; - - let mut app = self.app.lock().await; - app.data.secrets.set_items(items); - } - - pub async fn get_replication_controllers(&self) { - let items: Vec = self - .get_namespaced_resources(ReplicationController::into) - .await; - - let mut app = self.app.lock().await; - app.data.rpl_ctrls.set_items(items); - } - - pub async fn get_deployments(&self) { - let items: Vec = self.get_namespaced_resources(Deployment::into).await; - - let mut app = self.app.lock().await; - app.data.deployments.set_items(items); - } - - pub async fn get_daemon_sets_jobs(&self) { - let items: Vec = self.get_namespaced_resources(DaemonSet::into).await; - - let mut app = self.app.lock().await; - app.data.daemon_sets.set_items(items); - } - - pub async fn get_storage_classes(&self) { - let items: Vec = self.get_resources(StorageClass::into).await; - - let mut app = self.app.lock().await; - app.data.storage_classes.set_items(items); - } - - pub async fn get_roles(&self) { - let items: Vec = self.get_namespaced_resources(Role::into).await; - - let mut app = self.app.lock().await; - app.data.roles.set_items(items); - } - - pub async fn get_role_bindings(&self) { - let items: Vec = self.get_namespaced_resources(RoleBinding::into).await; - - let mut app = self.app.lock().await; - app.data.role_bindings.set_items(items); - } - - pub async fn get_cluster_roles(&self) { - let items: Vec = self.get_resources(ClusterRole::into).await; - - let mut app = self.app.lock().await; - app.data.cluster_roles.set_items(items); - } - - pub async fn get_cluster_role_binding(&self) { - let items: Vec = self.get_resources(ClusterRoleBinding::into).await; - - let mut app = self.app.lock().await; - app.data.cluster_role_bindings.set_items(items); - } - - pub async fn get_ingress(&self) { - let items: Vec = self.get_namespaced_resources(Ingress::into).await; - - let mut app = self.app.lock().await; - app.data.ingress.set_items(items); - } - - pub async fn get_pvcs(&self) { - let items: Vec = self - .get_namespaced_resources(PersistentVolumeClaim::into) - .await; - - let mut app = self.app.lock().await; - app.data.pvcs.set_items(items); - } - - pub async fn get_pvs(&self) { - let items: Vec = self.get_resources(PersistentVolume::into).await; - - let mut app = self.app.lock().await; - app.data.pvs.set_items(items); - } - - pub async fn get_service_accounts(&self) { - let items: Vec = self.get_namespaced_resources(ServiceAccount::into).await; - - let mut app = self.app.lock().await; - app.data.service_accounts.set_items(items); - } - - pub async fn get_network_policies(&self) { - let items: Vec = self.get_namespaced_resources(NetworkPolicy::into).await; - - let mut app = self.app.lock().await; - app.data.nw_policies.set_items(items); - } - - /// calls the kubernetes API to list the given resource for either selected namespace or all namespaces - async fn get_namespaced_resources(&self, map_fn: F) -> Vec - where - ::DynamicType: Default, - K: kube::Resource, - K: Clone + DeserializeOwned + fmt::Debug, - F: Fn(K) -> T, - { - let api: Api = self.get_namespaced_api().await; - let lp = ListParams::default(); - match api.list(&lp).await { - Ok(list) => list.into_iter().map(map_fn).collect::>(), - Err(e) => { - self - .handle_error(anyhow!( - "Failed to get namespaced resource {}. {:?}", - std::any::type_name::(), - e - )) - .await; - vec![] - } - } - } - - /// calls the kubernetes API to list the given resource for all namespaces - async fn get_resources(&self, map_fn: F) -> Vec - where - ::DynamicType: Default, - K: Clone + DeserializeOwned + fmt::Debug, - F: Fn(K) -> T, - { - let api: Api = Api::all(self.client.clone()); - let lp = ListParams::default(); - match api.list(&lp).await { - Ok(list) => list.into_iter().map(map_fn).collect::>(), - Err(e) => { - self - .handle_error(anyhow!( - "Failed to get resource {}. {:?}", - std::any::type_name::(), - e - )) - .await; - vec![] - } - } - } - - async fn get_namespaced_api(&self) -> Api - where - ::DynamicType: Default, - K: kube::Resource, - { - let app = self.app.lock().await; - match &app.data.selected.ns { - Some(ns) => Api::namespaced(self.client.clone(), ns), - None => Api::all(self.client.clone()), - } - } -} diff --git a/src/network/mod.rs b/src/network/mod.rs index 48c4435e..93a68e10 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -1,17 +1,44 @@ -// adapted from https://github.com/Rigellute/spotify-tui -mod dynamic_resource; -mod kube_api; pub(crate) mod stream; use core::convert::TryFrom; -use std::sync::Arc; +use std::{fmt, sync::Arc}; +use crate::app::{ + configmaps::ConfigMapResource, + contexts, + cronjobs::CronJobResource, + daemonsets::DaemonSetResource, + deployments::DeploymentResource, + dynamic::{DynamicResource, KubeDynamicKind}, + ingress::IngressResource, + jobs::JobResource, + metrics::UtilizationResource, + models::{AppResource, StatefulList}, + network_policies::NetworkPolicyResource, + nodes::NodeResource, + ns::NamespaceResource, + pods::PodResource, + pvcs::PvcResource, + pvs::PvResource, + replicasets::ReplicaSetResource, + replication_controllers::ReplicationControllerResource, + roles::{ClusterRoleBindingResource, ClusterRoleResource, RoleBindingResource, RoleResource}, + secrets::SecretResource, + serviceaccounts::SvcAcctResource, + statefulsets::StatefulSetResource, + storageclass::StorageClassResource, + svcs::SvcResource, + ActiveBlock, App, +}; use anyhow::{anyhow, Result}; -use kube::Client; +use k8s_openapi::NamespaceResourceScope; +use kube::{ + api::ListParams, config::Kubeconfig, discovery::verbs, Api, Client, Discovery, + Resource as ApiResource, +}; +use serde::de::DeserializeOwned; use tokio::sync::Mutex; -use crate::app::App; - #[derive(Debug, Eq, PartialEq)] pub enum IoEvent { GetKubeConfig, @@ -126,82 +153,82 @@ impl<'a> Network<'a> { self.get_kube_config().await; } IoEvent::GetNodes => { - self.get_nodes().await; + NodeResource::get_resource(self).await; } IoEvent::GetNamespaces => { - self.get_namespaces().await; + NamespaceResource::get_resource(self).await; } IoEvent::GetPods => { - self.get_pods().await; + PodResource::get_resource(self).await; } IoEvent::GetServices => { - self.get_services().await; + SvcResource::get_resource(self).await; } IoEvent::GetConfigMaps => { - self.get_config_maps().await; + ConfigMapResource::get_resource(self).await; } IoEvent::GetStatefulSets => { - self.get_stateful_sets().await; + StatefulSetResource::get_resource(self).await; } IoEvent::GetReplicaSets => { - self.get_replica_sets().await; + ReplicaSetResource::get_resource(self).await; } IoEvent::GetJobs => { - self.get_jobs().await; + JobResource::get_resource(self).await; } IoEvent::GetDaemonSets => { - self.get_daemon_sets_jobs().await; + DaemonSetResource::get_resource(self).await; } IoEvent::GetCronJobs => { - self.get_cron_jobs().await; + CronJobResource::get_resource(self).await; } IoEvent::GetSecrets => { - self.get_secrets().await; + SecretResource::get_resource(self).await; } IoEvent::GetDeployments => { - self.get_deployments().await; + DeploymentResource::get_resource(self).await; } IoEvent::GetReplicationControllers => { - self.get_replication_controllers().await; + ReplicationControllerResource::get_resource(self).await; } IoEvent::GetMetrics => { - self.get_utilizations().await; + UtilizationResource::get_resource(self).await; } IoEvent::GetStorageClasses => { - self.get_storage_classes().await; + StorageClassResource::get_resource(self).await; } IoEvent::GetRoles => { - self.get_roles().await; + RoleResource::get_resource(self).await; } IoEvent::GetRoleBindings => { - self.get_role_bindings().await; + RoleBindingResource::get_resource(self).await; } IoEvent::GetClusterRoles => { - self.get_cluster_roles().await; + ClusterRoleResource::get_resource(self).await; } IoEvent::GetClusterRoleBindings => { - self.get_cluster_role_binding().await; + ClusterRoleBindingResource::get_resource(self).await; } IoEvent::GetIngress => { - self.get_ingress().await; + IngressResource::get_resource(self).await; } IoEvent::GetPvcs => { - self.get_pvcs().await; + PvcResource::get_resource(self).await; } IoEvent::GetPvs => { - self.get_pvs().await; + PvResource::get_resource(self).await; } IoEvent::GetServiceAccounts => { - self.get_service_accounts().await; + SvcAcctResource::get_resource(self).await; } IoEvent::GetNetworkPolicies => { - self.get_network_policies().await; + NetworkPolicyResource::get_resource(self).await; } IoEvent::DiscoverDynamicRes => { self.discover_dynamic_resources().await; } IoEvent::GetDynamicRes => { - self.get_dynamic_resources().await; + DynamicResource::get_resource(self).await; } }; @@ -209,8 +236,143 @@ impl<'a> Network<'a> { app.is_loading = false; } - async fn handle_error(&self, e: anyhow::Error) { + pub async fn handle_error(&self, e: anyhow::Error) { let mut app = self.app.lock().await; app.handle_error(e); } + + pub async fn get_kube_config(&self) { + match Kubeconfig::read() { + Ok(config) => { + let mut app = self.app.lock().await; + let selected_ctx = app.data.selected.context.to_owned(); + app.set_contexts(contexts::get_contexts(&config, selected_ctx)); + app.data.kubeconfig = Some(config); + } + Err(e) => { + self + .handle_error(anyhow!("Failed to load Kubernetes config. {:?}", e)) + .await; + } + } + } + + /// calls the kubernetes API to list the given resource for either selected namespace or all namespaces + pub async fn get_namespaced_resources(&self, map_fn: F) -> Vec + where + ::DynamicType: Default, + K: kube::Resource, + K: Clone + DeserializeOwned + fmt::Debug, + F: Fn(K) -> T, + { + let api: Api = self.get_namespaced_api().await; + let lp = ListParams::default(); + match api.list(&lp).await { + Ok(list) => list.into_iter().map(map_fn).collect::>(), + Err(e) => { + self + .handle_error(anyhow!( + "Failed to get namespaced resource {}. {:?}", + std::any::type_name::(), + e + )) + .await; + vec![] + } + } + } + + /// calls the kubernetes API to list the given resource for all namespaces + pub async fn get_resources(&self, map_fn: F) -> Vec + where + ::DynamicType: Default, + K: Clone + DeserializeOwned + fmt::Debug, + F: Fn(K) -> T, + { + let api: Api = Api::all(self.client.clone()); + let lp = ListParams::default(); + match api.list(&lp).await { + Ok(list) => list.into_iter().map(map_fn).collect::>(), + Err(e) => { + self + .handle_error(anyhow!( + "Failed to get resource {}. {:?}", + std::any::type_name::(), + e + )) + .await; + vec![] + } + } + } + + pub async fn get_namespaced_api(&self) -> Api + where + ::DynamicType: Default, + K: kube::Resource, + { + let app = self.app.lock().await; + match &app.data.selected.ns { + Some(ns) => Api::namespaced(self.client.clone(), ns), + None => Api::all(self.client.clone()), + } + } + + /// Discover and cache custom resources on the cluster + pub async fn discover_dynamic_resources(&self) { + let discovery = match Discovery::new(self.client.clone()).run().await { + Ok(d) => d, + Err(e) => { + self + .handle_error(anyhow!("Failed to get dynamic resources. {:?}", e)) + .await; + return; + } + }; + + let mut dynamic_resources = vec![]; + let mut dynamic_menu = vec![]; + + let excluded = vec![ + "Namespace", + "Pod", + "Service", + "Node", + "ConfigMap", + "StatefulSet", + "ReplicaSet", + "Deployment", + "Job", + "DaemonSet", + "CronJob", + "Secret", + "ReplicationController", + "PersistentVolumeClaim", + "PersistentVolume", + "StorageClass", + "Role", + "RoleBinding", + "ClusterRole", + "ClusterRoleBinding", + "ServiceAccount", + "Ingress", + "NetworkPolicy", + ]; + + for group in discovery.groups() { + for (ar, caps) in group.recommended_resources() { + if !caps.supports_operation(verbs::LIST) || excluded.contains(&ar.kind.as_str()) { + continue; + } + + dynamic_menu.push((ar.kind.to_string(), ActiveBlock::DynamicResource)); + dynamic_resources.push(KubeDynamicKind::new(ar, caps.scope)); + } + } + let mut app = self.app.lock().await; + // sort dynamic_menu alphabetically using the first element of the tuple + dynamic_menu.sort_by(|a, b| a.0.cmp(&b.0)); + app.dynamic_resources_menu = StatefulList::with_items(dynamic_menu); + app.data.dynamic_kinds = dynamic_resources.clone(); + } } diff --git a/src/ui/contexts.rs b/src/ui/contexts.rs deleted file mode 100644 index c84e680b..00000000 --- a/src/ui/contexts.rs +++ /dev/null @@ -1,54 +0,0 @@ -use tui::{ - backend::Backend, - layout::{Constraint, Rect}, - widgets::{Cell, Row, Table}, - Frame, -}; - -use super::{ - utils::{ - layout_block_active, loading, style_highlight, style_primary, style_secondary, - table_header_style, - }, - HIGHLIGHT, -}; -use crate::app::App; - -pub fn draw_contexts(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = format!(" Contexts [{}] ", app.data.contexts.items.len()); - let block = layout_block_active(title.as_str(), app.light_theme); - - if !app.data.contexts.items.is_empty() { - let rows = app.data.contexts.items.iter().map(|c| { - let style = if c.is_active { - style_secondary(app.light_theme) - } else { - style_primary(app.light_theme) - }; - Row::new(vec![ - Cell::from(c.name.as_ref()), - Cell::from(c.cluster.as_ref()), - Cell::from(c.user.as_ref()), - ]) - .style(style) - }); - - let table = Table::new(rows) - .header(table_header_style( - vec!["Context", "Cluster", "User"], - app.light_theme, - )) - .block(block) - .widths(&[ - Constraint::Percentage(34), - Constraint::Percentage(33), - Constraint::Percentage(33), - ]) - .highlight_style(style_highlight()) - .highlight_symbol(HIGHLIGHT); - - f.render_stateful_widget(table, area, &mut app.data.contexts.state); - } else { - loading(f, block, area, app.is_loading, app.light_theme); - } -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 3bbb6bae..0df4a6e2 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,9 +1,7 @@ -mod contexts; mod help; mod overview; -mod resource_tabs; -mod utilization; -mod utils; +pub mod resource_tabs; +pub mod utils; use tui::{ backend::Backend, @@ -14,18 +12,19 @@ use tui::{ }; use self::{ - contexts::draw_contexts, help::draw_help, overview::draw_overview, - utilization::draw_utilization, utils::{ horizontal_chunks_with_margin, layout_block, style_default, style_failure, style_help, style_main_background, style_primary, style_secondary, title_style_logo, vertical_chunks, }, }; -use crate::app::{App, RouteId}; +use crate::app::{ + contexts::ContextResource, metrics::UtilizationResource, models::AppResource, ActiveBlock, App, + RouteId, +}; -static HIGHLIGHT: &str = "=> "; +pub static HIGHLIGHT: &str = "=> "; pub fn draw(f: &mut Frame<'_, B>, app: &mut App) { let block = Block::default().style(style_main_background(app.light_theme)); @@ -55,10 +54,10 @@ pub fn draw(f: &mut Frame<'_, B>, app: &mut App) { draw_help(f, app, last_chunk); } RouteId::Contexts => { - draw_contexts(f, app, last_chunk); + ContextResource::render(ActiveBlock::Contexts, f, app, last_chunk); } RouteId::Utilization => { - draw_utilization(f, app, last_chunk); + UtilizationResource::render(ActiveBlock::Utilization, f, app, last_chunk); } _ => { draw_overview(f, app, last_chunk); diff --git a/src/ui/overview.rs b/src/ui/overview.rs index 0fe9c9fc..b2370cce 100644 --- a/src/ui/overview.rs +++ b/src/ui/overview.rs @@ -10,13 +10,11 @@ use super::{ resource_tabs::draw_resource_tabs_block, utils::{ get_gauge_style, horizontal_chunks, layout_block_default, loading, style_default, - style_failure, style_highlight, style_logo, style_primary, style_secondary, table_header_style, - vertical_chunks, vertical_chunks_with_margin, + style_failure, style_logo, style_primary, vertical_chunks, vertical_chunks_with_margin, }, - HIGHLIGHT, }; use crate::{ - app::{key_binding::DEFAULT_KEYBINDING, metrics::KubeNodeMetrics, ActiveBlock, App}, + app::{metrics::KubeNodeMetrics, models::AppResource, ns::NamespaceResource, ActiveBlock, App}, banner::BANNER, }; @@ -41,7 +39,7 @@ fn draw_status_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect area, ); - draw_namespaces_block(f, app, chunks[0]); + NamespaceResource::render(ActiveBlock::Namespaces, f, app, chunks[0]); draw_context_info_block(f, app, chunks[1]); draw_cli_version_block(f, app, chunks[2]); draw_logo_block(f, app, chunks[3]) @@ -154,44 +152,6 @@ fn draw_context_info_block(f: &mut Frame<'_, B>, app: &mut App, area f.render_widget(mem_gauge, chunks[2]); } -fn draw_namespaces_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = format!( - " Namespaces {} (all: {}) ", - DEFAULT_KEYBINDING.jump_to_namespace.key, DEFAULT_KEYBINDING.select_all_namespace.key - ); - let mut block = layout_block_default(title.as_str()); - - if app.get_current_route().active_block == ActiveBlock::Namespaces { - block = block.style(style_secondary(app.light_theme)) - } - - if !app.data.namespaces.items.is_empty() { - let rows = app.data.namespaces.items.iter().map(|s| { - let style = if Some(s.name.clone()) == app.data.selected.ns { - style_secondary(app.light_theme) - } else { - style_primary(app.light_theme) - }; - Row::new(vec![ - Cell::from(s.name.as_ref()), - Cell::from(s.status.as_ref()), - ]) - .style(style) - }); - - let table = Table::new(rows) - .header(table_header_style(vec!["Name", "Status"], app.light_theme)) - .block(block) - .highlight_style(style_highlight()) - .highlight_symbol(HIGHLIGHT) - .widths(&[Constraint::Length(22), Constraint::Length(6)]); - - f.render_stateful_widget(table, area, &mut app.data.namespaces.state); - } else { - loading(f, block, area, app.is_loading, app.light_theme); - } -} - // Utility methods /// covert percent value from metrics to ratio that gauge can understand diff --git a/src/ui/resource_tabs.rs b/src/ui/resource_tabs.rs index 20b635e8..54a10053 100644 --- a/src/ui/resource_tabs.rs +++ b/src/ui/resource_tabs.rs @@ -1,56 +1,43 @@ -use kube::discovery::Scope; use tui::{ backend::Backend, layout::{Constraint, Rect}, - style::Style, - text::{Span, Spans, Text}, - widgets::{Cell, List, ListItem, Paragraph, Row, Table, Tabs, Wrap}, + text::{Span, Spans}, + widgets::{List, ListItem, Tabs}, Frame, }; use super::{ utils::{ - centered_rect, layout_block_default, layout_block_top_border, loading, style_default, - style_failure, style_highlight, style_primary, style_secondary, style_success, - table_header_style, title_with_dual_style, vertical_chunks_with_margin, + centered_rect, layout_block_default, style_default, style_highlight, style_secondary, + vertical_chunks_with_margin, }, HIGHLIGHT, }; use crate::app::{ - models::{StatefulList, StatefulTable}, + configmaps::ConfigMapResource, + cronjobs::CronJobResource, + daemonsets::DaemonSetResource, + deployments::DeploymentResource, + dynamic::DynamicResource, + ingress::IngressResource, + jobs::JobResource, + models::{AppResource, StatefulList}, + network_policies::NetworkPolicyResource, + nodes::NodeResource, + pods::PodResource, + pvcs::PvcResource, + pvs::PvResource, + replicasets::ReplicaSetResource, + replication_controllers::ReplicationControllerResource, + roles::{ClusterRoleBindingResource, ClusterRoleResource, RoleBindingResource, RoleResource}, + secrets::SecretResource, + serviceaccounts::SvcAcctResource, + statefulsets::StatefulSetResource, + storageclass::StorageClassResource, + svcs::SvcResource, ActiveBlock, App, }; -static DESCRIBE_AND_YAML_HINT: &str = "| describe | yaml "; -static DESCRIBE_YAML_AND_ESC_HINT: &str = "| describe | yaml | back to menu "; -static DESCRIBE_YAML_DECODE_AND_ESC_HINT: &str = - "| describe | yaml | decode | back to menu "; -static COPY_HINT: &str = "| copy "; -static NODES_TITLE: &str = "Nodes"; -static PODS_TITLE: &str = "Pods"; -static SERVICES_TITLE: &str = "Services"; -static CONFIG_MAPS_TITLE: &str = "ConfigMaps"; -static STFS_TITLE: &str = "StatefulSets"; -static REPLICA_SETS_TITLE: &str = "ReplicaSets"; -static DEPLOYMENTS_TITLE: &str = "Deployments"; -static JOBS_TITLE: &str = "Jobs"; -static DAEMON_SETS_TITLE: &str = "DaemonSets"; -static CRON_JOBS_TITLE: &str = "CronJobs"; -static SECRETS_TITLE: &str = "Secrets"; -static RPL_CTRL_TITLE: &str = "ReplicationControllers"; -static STORAGE_CLASSES_LABEL: &str = "StorageClasses"; -static ROLES_TITLE: &str = "Roles"; -static ROLE_BINDINGS_TITLE: &str = "RoleBindings"; -static CLUSTER_ROLES_TITLE: &str = "ClusterRoles"; -static CLUSTER_ROLES_BINDING_TITLE: &str = "ClusterRoleBinding"; -static INGRESS_TITLE: &str = "Ingresses"; -static PVC_TITLE: &str = "PersistentVolumeClaims"; -static PV_TITLE: &str = "PersistentVolumes"; -static SVC_ACCT_TITLE: &str = "ServiceAccounts"; -static NW_POLICY_TITLE: &str = "NetworkPolicies"; -static DESCRIBE_ACTIVE: &str = "-> Describe "; -static YAML_ACTIVE: &str = "-> YAML "; - pub fn draw_resource_tabs_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let chunks = vertical_chunks_with_margin(vec![Constraint::Length(2), Constraint::Min(0)], area, 1); @@ -75,15 +62,15 @@ pub fn draw_resource_tabs_block(f: &mut Frame<'_, B>, app: &mut App, // render tab content match app.context_tabs.index { - 0 => draw_pods_tab(app.get_current_route().active_block, f, app, chunks[1]), - 1 => draw_services_tab(app.get_current_route().active_block, f, app, chunks[1]), - 2 => draw_nodes_tab(app.get_current_route().active_block, f, app, chunks[1]), - 3 => draw_config_maps_tab(app.get_current_route().active_block, f, app, chunks[1]), - 4 => draw_stateful_sets_tab(app.get_current_route().active_block, f, app, chunks[1]), - 5 => draw_replica_sets_tab(app.get_current_route().active_block, f, app, chunks[1]), - 6 => draw_deployments_tab(app.get_current_route().active_block, f, app, chunks[1]), - 7 => draw_jobs_tab(app.get_current_route().active_block, f, app, chunks[1]), - 8 => draw_daemon_sets_tab(app.get_current_route().active_block, f, app, chunks[1]), + 0 => PodResource::render(app.get_current_route().active_block, f, app, chunks[1]), + 1 => SvcResource::render(app.get_current_route().active_block, f, app, chunks[1]), + 2 => NodeResource::render(app.get_current_route().active_block, f, app, chunks[1]), + 3 => ConfigMapResource::render(app.get_current_route().active_block, f, app, chunks[1]), + 4 => StatefulSetResource::render(app.get_current_route().active_block, f, app, chunks[1]), + 5 => ReplicaSetResource::render(app.get_current_route().active_block, f, app, chunks[1]), + 6 => DeploymentResource::render(app.get_current_route().active_block, f, app, chunks[1]), + 7 => JobResource::render(app.get_current_route().active_block, f, app, chunks[1]), + 8 => DaemonSetResource::render(app.get_current_route().active_block, f, app, chunks[1]), 9 | 10 => draw_more(app.get_current_route().active_block, f, app, chunks[1]), _ => {} }; @@ -94,40 +81,40 @@ fn draw_more(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App match block { ActiveBlock::More => draw_menu(f, &mut app.more_resources_menu, area), ActiveBlock::DynamicView => draw_menu(f, &mut app.dynamic_resources_menu, area), - ActiveBlock::CronJobs => draw_cronjobs_tab(block, f, app, area), - ActiveBlock::Secrets => draw_secrets_tab(block, f, app, area), - ActiveBlock::RplCtrl => draw_replication_controllers_tab(block, f, app, area), - ActiveBlock::StorageClasses => draw_storage_classes_tab(block, f, app, area), - ActiveBlock::Roles => draw_roles_tab(block, f, app, area), - ActiveBlock::RoleBindings => draw_role_bindings_tab(block, f, app, area), - ActiveBlock::ClusterRoles => draw_cluster_roles_tab(block, f, app, area), - ActiveBlock::ClusterRoleBinding => draw_cluster_role_binding_tab(block, f, app, area), - ActiveBlock::Ingress => draw_ingress_tab(block, f, app, area), - ActiveBlock::Pvc => draw_pvc_tab(block, f, app, area), - ActiveBlock::Pv => draw_pv_tab(block, f, app, area), - ActiveBlock::ServiceAccounts => draw_svc_acct_tab(block, f, app, area), - ActiveBlock::NetworkPolicies => draw_nw_policy_tab(block, f, app, area), - ActiveBlock::DynamicResource => draw_dynamic_res_tab(block, f, app, area), + ActiveBlock::CronJobs => CronJobResource::render(block, f, app, area), + ActiveBlock::Secrets => SecretResource::render(block, f, app, area), + ActiveBlock::RplCtrl => ReplicationControllerResource::render(block, f, app, area), + ActiveBlock::StorageClasses => StorageClassResource::render(block, f, app, area), + ActiveBlock::Roles => RoleResource::render(block, f, app, area), + ActiveBlock::RoleBindings => RoleBindingResource::render(block, f, app, area), + ActiveBlock::ClusterRoles => ClusterRoleResource::render(block, f, app, area), + ActiveBlock::ClusterRoleBinding => ClusterRoleBindingResource::render(block, f, app, area), + ActiveBlock::Ingress => IngressResource::render(block, f, app, area), + ActiveBlock::Pvc => PvcResource::render(block, f, app, area), + ActiveBlock::Pv => PvResource::render(block, f, app, area), + ActiveBlock::ServiceAccounts => SvcAcctResource::render(block, f, app, area), + ActiveBlock::NetworkPolicies => NetworkPolicyResource::render(block, f, app, area), + ActiveBlock::DynamicResource => DynamicResource::render(block, f, app, area), ActiveBlock::Describe | ActiveBlock::Yaml => { let mut prev_route = app.get_prev_route(); if prev_route.active_block == block { prev_route = app.get_nth_route_from_last(2); } match prev_route.active_block { - ActiveBlock::CronJobs => draw_cronjobs_tab(block, f, app, area), - ActiveBlock::Secrets => draw_secrets_tab(block, f, app, area), - ActiveBlock::RplCtrl => draw_replication_controllers_tab(block, f, app, area), - ActiveBlock::StorageClasses => draw_storage_classes_tab(block, f, app, area), - ActiveBlock::Roles => draw_roles_tab(block, f, app, area), - ActiveBlock::RoleBindings => draw_role_bindings_tab(block, f, app, area), - ActiveBlock::ClusterRoles => draw_cluster_roles_tab(block, f, app, area), - ActiveBlock::ClusterRoleBinding => draw_cluster_role_binding_tab(block, f, app, area), - ActiveBlock::Ingress => draw_ingress_tab(block, f, app, area), - ActiveBlock::Pvc => draw_pvc_tab(block, f, app, area), - ActiveBlock::Pv => draw_pv_tab(block, f, app, area), - ActiveBlock::ServiceAccounts => draw_svc_acct_tab(block, f, app, area), - ActiveBlock::NetworkPolicies => draw_nw_policy_tab(block, f, app, area), - ActiveBlock::DynamicResource => draw_dynamic_res_tab(block, f, app, area), + ActiveBlock::CronJobs => CronJobResource::render(block, f, app, area), + ActiveBlock::Secrets => SecretResource::render(block, f, app, area), + ActiveBlock::RplCtrl => ReplicationControllerResource::render(block, f, app, area), + ActiveBlock::StorageClasses => StorageClassResource::render(block, f, app, area), + ActiveBlock::Roles => RoleResource::render(block, f, app, area), + ActiveBlock::RoleBindings => RoleBindingResource::render(block, f, app, area), + ActiveBlock::ClusterRoles => ClusterRoleResource::render(block, f, app, area), + ActiveBlock::ClusterRoleBinding => ClusterRoleBindingResource::render(block, f, app, area), + ActiveBlock::Ingress => IngressResource::render(block, f, app, area), + ActiveBlock::Pvc => PvcResource::render(block, f, app, area), + ActiveBlock::Pv => PvResource::render(block, f, app, area), + ActiveBlock::ServiceAccounts => SvcAcctResource::render(block, f, app, area), + ActiveBlock::NetworkPolicies => NetworkPolicyResource::render(block, f, app, area), + ActiveBlock::DynamicResource => DynamicResource::render(block, f, app, area), _ => { /* do nothing */ } } } @@ -159,1611 +146,19 @@ fn draw_menu( ); } -// using a macro to reuse code as generics will make handling lifetimes a PITA -macro_rules! draw_resource_tab { - ($title:expr, $block:expr, $f:expr, $app:expr, $area:expr, $fn1:expr, $fn2:expr, $res:expr) => { - match $block { - ActiveBlock::Describe | ActiveBlock::Yaml => draw_describe_block( - $f, - $app, - $area, - title_with_dual_style( - get_resource_title($app, $title, get_describe_active($block), $res.items.len()), - format!("{} | {} ", COPY_HINT, $title), - $app.light_theme, - ), - ), - ActiveBlock::Namespaces => $fn1($app.get_prev_route().active_block, $f, $app, $area), - _ => $fn2($f, $app, $area), - }; - }; -} - -fn draw_pods_tab(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - match block { - ActiveBlock::Containers => draw_containers_block(f, app, area), - ActiveBlock::Describe | ActiveBlock::Yaml => draw_describe_block( - f, - app, - area, - title_with_dual_style( - get_resource_title( - app, - PODS_TITLE, - get_describe_active(block), - app.data.pods.items.len(), - ), - format!("{} | {} ", COPY_HINT, PODS_TITLE), - app.light_theme, - ), - ), - ActiveBlock::Logs => draw_logs_block(f, app, area), - ActiveBlock::Namespaces => draw_pods_tab(app.get_prev_route().active_block, f, app, area), - _ => draw_pods_block(f, app, area), - }; -} - -fn draw_pods_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, PODS_TITLE, "", app.data.pods.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: format!("| Containers {}", DESCRIBE_AND_YAML_HINT), - resource: &mut app.data.pods, - table_headers: vec!["Namespace", "Name", "Ready", "Status", "Restarts", "Age"], - column_widths: vec![ - Constraint::Percentage(25), - Constraint::Percentage(35), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - let style = get_resource_row_style(c.status.as_str(), c.ready, app.light_theme); - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(format!("{}/{}", c.ready.0, c.ready.1)), - Cell::from(c.status.to_owned()), - Cell::from(c.restarts.to_string()), - Cell::from(c.age.to_owned()), - ]) - .style(style) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_containers_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_container_title(app, app.data.containers.items.len(), ""); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: format!("| Logs | {} ", PODS_TITLE), - resource: &mut app.data.containers, - table_headers: vec![ - "Name", - "Image", - "Init", - "Ready", - "State", - "Restarts", - "Probes(L/R)", - "Ports", - "Age", - ], - column_widths: vec![ - Constraint::Percentage(20), - Constraint::Percentage(25), - Constraint::Percentage(5), - Constraint::Percentage(5), - Constraint::Percentage(10), - Constraint::Percentage(5), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - let style = get_resource_row_style(c.status.as_str(), (0, 0), app.light_theme); - Row::new(vec![ - Cell::from(c.name.to_owned()), - Cell::from(c.image.to_owned()), - Cell::from(c.init.to_string()), - Cell::from(c.ready.to_owned()), - Cell::from(c.status.to_owned()), - Cell::from(c.restarts.to_string()), - Cell::from(format!("{}/{}", c.liveliness_probe, c.readiness_probe,)), - Cell::from(c.ports.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_logs_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let selected_container = app.data.selected.container.clone(); - let container_name = selected_container.unwrap_or_default(); - - let title = title_with_dual_style( - get_container_title( - app, - app.data.containers.items.len(), - format!("-> Logs ({}) ", container_name), - ), - "| copy | Containers ".into(), - app.light_theme, - ); - - let block = layout_block_top_border(title); - - if container_name == app.data.logs.id { - app.data.logs.render_list( - f, - area, - block, - style_primary(app.light_theme), - app.log_auto_scroll, - ); - } else { - loading(f, block, area, app.is_loading, app.light_theme); - } -} - -fn draw_nodes_tab(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - match block { - ActiveBlock::Describe | ActiveBlock::Yaml => draw_describe_block( - f, - app, - area, - title_with_dual_style( - get_cluster_wide_resource_title( - NODES_TITLE, - app.data.nodes.items.len(), - get_describe_active(block), - ), - format!("{} | {} ", COPY_HINT, NODES_TITLE), - app.light_theme, - ), - ), - ActiveBlock::Namespaces => draw_nodes_tab(app.get_prev_route().active_block, f, app, area), - _ => draw_nodes_block(f, app, area), - }; -} - -fn draw_nodes_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_cluster_wide_resource_title(NODES_TITLE, app.data.nodes.items.len(), ""); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_AND_YAML_HINT.into(), - resource: &mut app.data.nodes, - table_headers: vec![ - "Name", "Status", "Roles", "Version", PODS_TITLE, "CPU", "Mem", "CPU %", "Mem %", "CPU/A", - "Mem/A", "Age", - ], - column_widths: vec![ - Constraint::Percentage(25), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(5), - Constraint::Percentage(5), - Constraint::Percentage(5), - Constraint::Percentage(5), - Constraint::Percentage(5), - Constraint::Percentage(5), - Constraint::Percentage(5), - Constraint::Percentage(10), - ], - }, - |c| { - let style = if c.status != "Ready" { - style_failure(app.light_theme) - } else { - style_primary(app.light_theme) - }; - Row::new(vec![ - Cell::from(c.name.to_owned()), - Cell::from(c.status.to_owned()), - Cell::from(c.role.to_owned()), - Cell::from(c.version.to_owned()), - Cell::from(c.pods.to_string()), - Cell::from(c.cpu.to_owned()), - Cell::from(c.mem.to_owned()), - Cell::from(c.cpu_percent.to_owned()), - Cell::from(c.mem_percent.to_owned()), - Cell::from(c.cpu_a.to_owned()), - Cell::from(c.mem_a.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_services_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - SERVICES_TITLE, - block, - f, - app, - area, - draw_services_tab, - draw_services_block, - app.data.services - ); -} - -fn draw_services_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, SERVICES_TITLE, "", app.data.services.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_AND_YAML_HINT.into(), - resource: &mut app.data.services, - table_headers: vec![ - "Namespace", - "Name", - "Type", - "Cluster IP", - "External IP", - "Ports", - "Age", - ], - column_widths: vec![ - Constraint::Percentage(10), - Constraint::Percentage(25), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(15), - Constraint::Percentage(20), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.type_.to_owned()), - Cell::from(c.cluster_ip.to_owned()), - Cell::from(c.external_ip.to_owned()), - Cell::from(c.ports.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_config_maps_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - CONFIG_MAPS_TITLE, - block, - f, - app, - area, - draw_config_maps_tab, - draw_config_maps_block, - app.data.config_maps - ); -} - -fn draw_config_maps_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, CONFIG_MAPS_TITLE, "", app.data.config_maps.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_AND_YAML_HINT.into(), - resource: &mut app.data.config_maps, - table_headers: vec!["Namespace", "Name", "Data", "Age"], - column_widths: vec![ - Constraint::Percentage(30), - Constraint::Percentage(40), - Constraint::Percentage(15), - Constraint::Percentage(15), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.data.len().to_string()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_stateful_sets_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - STFS_TITLE, - block, - f, - app, - area, - draw_stateful_sets_tab, - draw_stateful_sets_block, - app.data.stateful_sets - ); -} - -fn draw_stateful_sets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, STFS_TITLE, "", app.data.stateful_sets.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_AND_YAML_HINT.into(), - resource: &mut app.data.stateful_sets, - table_headers: vec!["Namespace", "Name", "Ready", "Service", "Age"], - column_widths: vec![ - Constraint::Percentage(25), - Constraint::Percentage(30), - Constraint::Percentage(10), - Constraint::Percentage(25), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.ready.to_owned()), - Cell::from(c.service.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_replica_sets_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - REPLICA_SETS_TITLE, - block, - f, - app, - area, - draw_replica_sets_tab, - draw_replica_sets_block, - app.data.replica_sets - ); -} - -fn draw_replica_sets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title( - app, - REPLICA_SETS_TITLE, - "", - app.data.replica_sets.items.len(), - ); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_AND_YAML_HINT.into(), - resource: &mut app.data.replica_sets, - table_headers: vec!["Namespace", "Name", "Desired", "Current", "Ready", "Age"], - column_widths: vec![ - Constraint::Percentage(25), - Constraint::Percentage(35), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.desired.to_string()), - Cell::from(c.current.to_string()), - Cell::from(c.ready.to_string()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_deployments_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - DEPLOYMENTS_TITLE, - block, - f, - app, - area, - draw_deployments_tab, - draw_deployments_block, - app.data.deployments - ); -} - -fn draw_deployments_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, DEPLOYMENTS_TITLE, "", app.data.deployments.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_AND_YAML_HINT.into(), - resource: &mut app.data.deployments, - table_headers: vec![ - "Namespace", - "Name", - "Ready", - "Up-to-date", - "Available", - "Age", - ], - column_widths: vec![ - Constraint::Percentage(25), - Constraint::Percentage(35), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.ready.to_owned()), - Cell::from(c.updated.to_string()), - Cell::from(c.available.to_string()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_jobs_tab(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - draw_resource_tab!( - JOBS_TITLE, - block, - f, - app, - area, - draw_jobs_tab, - draw_jobs_block, - app.data.jobs - ); -} - -fn draw_jobs_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, JOBS_TITLE, "", app.data.jobs.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_AND_YAML_HINT.into(), - resource: &mut app.data.jobs, - table_headers: vec!["Namespace", "Name", "Completions", "Duration", "Age"], - column_widths: vec![ - Constraint::Percentage(25), - Constraint::Percentage(40), - Constraint::Percentage(15), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.completions.to_owned()), - Cell::from(c.duration.to_string()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_daemon_sets_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - DAEMON_SETS_TITLE, - block, - f, - app, - area, - draw_daemon_sets_tab, - draw_daemon_sets_block, - app.data.daemon_sets - ); -} - -fn draw_daemon_sets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, DAEMON_SETS_TITLE, "", app.data.daemon_sets.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_AND_YAML_HINT.into(), - resource: &mut app.data.daemon_sets, - table_headers: vec![ - "Namespace", - "Name", - "Desired", - "Current", - "Ready", - "Up-to-date", - "Available", - "Age", - ], - column_widths: vec![ - Constraint::Percentage(20), - Constraint::Percentage(20), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.desired.to_string()), - Cell::from(c.current.to_string()), - Cell::from(c.ready.to_string()), - Cell::from(c.up_to_date.to_string()), - Cell::from(c.available.to_string()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_cronjobs_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - CRON_JOBS_TITLE, - block, - f, - app, - area, - draw_cronjobs_tab, - draw_cronjobs_block, - app.data.cronjobs - ); -} - -fn draw_cronjobs_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, CRON_JOBS_TITLE, "", app.data.cronjobs.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.cronjobs, - table_headers: vec![ - "Namespace", - "Name", - "Schedule", - "Last Scheduled", - "Suspend", - "Active", - "Age", - ], - column_widths: vec![ - Constraint::Percentage(20), - Constraint::Percentage(25), - Constraint::Percentage(15), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.schedule.to_owned()), - Cell::from(c.last_schedule.to_string()), - Cell::from(c.suspend.to_string()), - Cell::from(c.active.to_string()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_secrets_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - SECRETS_TITLE, - block, - f, - app, - area, - draw_secrets_tab, - draw_secrets_block, - app.data.secrets - ); -} - -fn draw_secrets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, SECRETS_TITLE, "", app.data.secrets.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_DECODE_AND_ESC_HINT.into(), - resource: &mut app.data.secrets, - table_headers: vec!["Namespace", "Name", "Type", "Data", "Age"], - column_widths: vec![ - Constraint::Percentage(25), - Constraint::Percentage(30), - Constraint::Percentage(25), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.type_.to_owned()), - Cell::from(c.data.len().to_string()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_replication_controllers_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - RPL_CTRL_TITLE, - block, - f, - app, - area, - draw_replication_controllers_tab, - draw_replication_controllers_block, - app.data.rpl_ctrls - ); -} - -fn draw_replication_controllers_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, RPL_CTRL_TITLE, "", app.data.rpl_ctrls.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.rpl_ctrls, - table_headers: vec![ - "Namespace", - "Name", - "Desired", - "Current", - "Ready", - "Containers", - "Images", - "Selector", - "Age", - ], - column_widths: vec![ - Constraint::Percentage(15), - Constraint::Percentage(15), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.desired.to_string()), - Cell::from(c.current.to_string()), - Cell::from(c.ready.to_string()), - Cell::from(c.containers.to_owned()), - Cell::from(c.images.to_owned()), - Cell::from(c.selector.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_storage_classes_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - STORAGE_CLASSES_LABEL, - block, - f, - app, - area, - draw_storage_classes_tab, - draw_storage_classes_block, - app.data.storage_classes - ); -} - -fn draw_storage_classes_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_cluster_wide_resource_title( - STORAGE_CLASSES_LABEL, - app.data.storage_classes.items.len(), - "", - ); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.storage_classes, - table_headers: vec![ - "Name", - "Provisioner", - "Reclaim Policy", - "Volume Binding Mode", - "Allow Volume Expansion", - "Age", - ], - column_widths: vec![ - Constraint::Percentage(10), - Constraint::Percentage(20), - Constraint::Percentage(10), - Constraint::Percentage(20), - Constraint::Percentage(20), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.name.to_owned()), - Cell::from(c.provisioner.to_owned()), - Cell::from(c.reclaim_policy.to_owned()), - Cell::from(c.volume_binding_mode.to_owned()), - Cell::from(c.allow_volume_expansion.to_string()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_svc_acct_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - SVC_ACCT_TITLE, - block, - f, - app, - area, - draw_svc_acct_tab, - draw_svc_acct_block, - app.data.service_accounts - ); -} - -fn draw_svc_acct_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title( - app, - SVC_ACCT_TITLE, - "", - app.data.service_accounts.items.len(), - ); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.service_accounts, - table_headers: vec!["Namespace", "Name", "Secrets", "Age"], - column_widths: vec![ - Constraint::Percentage(30), - Constraint::Percentage(30), - Constraint::Percentage(20), - Constraint::Percentage(20), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.secrets.to_string()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_nw_policy_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - NW_POLICY_TITLE, - block, - f, - app, - area, - draw_nw_policy_tab, - draw_nw_policy_block, - app.data.nw_policies - ); -} - -fn draw_nw_policy_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, NW_POLICY_TITLE, "", app.data.nw_policies.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.nw_policies, - table_headers: vec!["Namespace", "Name", "Pod Selector", "Policy Types", "Age"], - column_widths: vec![ - Constraint::Percentage(20), - Constraint::Percentage(20), - Constraint::Percentage(30), - Constraint::Percentage(20), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.pod_selector.to_owned()), - Cell::from(c.policy_types.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_roles_tab(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - draw_resource_tab!( - ROLES_TITLE, - block, - f, - app, - area, - draw_roles_tab, - draw_roles_block, - app.data.roles - ); -} - -fn draw_roles_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, ROLES_TITLE, "", app.data.roles.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.roles, - table_headers: vec!["Namespace", "Name", "Age"], - column_widths: vec![ - Constraint::Percentage(40), - Constraint::Percentage(40), - Constraint::Percentage(20), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_role_bindings_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - ROLE_BINDINGS_TITLE, - block, - f, - app, - area, - draw_role_bindings_tab, - draw_role_bindings_block, - app.data.role_bindings - ); -} - -fn draw_role_bindings_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title( - app, - ROLE_BINDINGS_TITLE, - "", - app.data.role_bindings.items.len(), - ); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.role_bindings, - table_headers: vec!["Namespace", "Name", "Role", "Age"], - column_widths: vec![ - Constraint::Percentage(20), - Constraint::Percentage(30), - Constraint::Percentage(30), - Constraint::Percentage(20), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.role.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_cluster_roles_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - CLUSTER_ROLES_TITLE, - block, - f, - app, - area, - draw_cluster_roles_tab, - draw_cluster_roles_block, - app.data.cluster_roles - ); -} - -fn draw_cluster_roles_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title( - app, - CLUSTER_ROLES_TITLE, - "", - app.data.cluster_roles.items.len(), - ); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.cluster_roles, - table_headers: vec!["Name", "Age"], - column_widths: vec![Constraint::Percentage(50), Constraint::Percentage(50)], - }, - |c| { - Row::new(vec![ - Cell::from(c.name.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_cluster_role_binding_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - CLUSTER_ROLES_BINDING_TITLE, - block, - f, - app, - area, - draw_cluster_role_binding_tab, - draw_cluster_role_binding_block, - app.data.cluster_role_bindings - ); -} - -fn draw_cluster_role_binding_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title( - app, - CLUSTER_ROLES_BINDING_TITLE, - "", - app.data.cluster_role_bindings.items.len(), - ); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.cluster_role_bindings, - table_headers: vec!["Name", "Role", "Age"], - column_widths: vec![ - Constraint::Percentage(40), - Constraint::Percentage(40), - Constraint::Percentage(20), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.name.to_owned()), - Cell::from(c.role.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_ingress_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - draw_resource_tab!( - INGRESS_TITLE, - block, - f, - app, - area, - draw_ingress_tab, - draw_ingress_block, - app.data.ingress - ); -} - -fn draw_ingress_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, INGRESS_TITLE, "", app.data.ingress.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.ingress, - table_headers: vec![ - "Namespace", - "Name", - "Ingress class", - "Paths", - "Default backend", - "Addresses", - "Age", - ], - column_widths: vec![ - Constraint::Percentage(10), - Constraint::Percentage(20), - Constraint::Percentage(10), - Constraint::Percentage(25), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.ingress_class.to_owned()), - Cell::from(c.paths.to_owned()), - Cell::from(c.default_backend.to_owned()), - Cell::from(c.address.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_pvc_tab(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - draw_resource_tab!( - PVC_TITLE, - block, - f, - app, - area, - draw_pvc_tab, - draw_pvc_block, - app.data.pvcs - ); -} - -fn draw_pvc_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, PVC_TITLE, "", app.data.pvcs.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.pvcs, - table_headers: vec![ - "Namespace", - "Name", - "Status", - "Volume", - "Capacity", - "Access Modes", - "Storage Class", - "Age", - ], - column_widths: vec![ - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(20), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.status.to_owned()), - Cell::from(c.volume.to_owned()), - Cell::from(c.capacity.to_owned()), - Cell::from(c.access_modes.to_owned()), - Cell::from(c.storage_class.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_pv_tab(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - draw_resource_tab!( - PV_TITLE, - block, - f, - app, - area, - draw_pv_tab, - draw_pv_block, - app.data.pvs - ); -} - -fn draw_pv_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = get_resource_title(app, PV_TITLE, "", app.data.pvs.items.len()); - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.pvs, - table_headers: vec![ - "Name", - "Capacity", - "Access Modes", - "Reclaim Policy", - "Status", - "Claim", - "Storage Class", - "Reason", - "Age", - ], - column_widths: vec![ - Constraint::Percentage(20), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.name.to_owned()), - Cell::from(c.capacity.to_owned()), - Cell::from(c.access_modes.to_owned()), - Cell::from(c.reclaim_policy.to_owned()), - Cell::from(c.status.to_owned()), - Cell::from(c.claim.to_owned()), - Cell::from(c.storage_class.to_owned()), - Cell::from(c.reason.to_owned()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -fn draw_dynamic_res_tab( - block: ActiveBlock, - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, -) { - let title = if let Some(res) = &app.data.selected.dynamic_kind { - res.kind.as_str() - } else { - "" - }; - draw_resource_tab!( - title, - block, - f, - app, - area, - draw_dynamic_res_tab, - draw_dynamic_res_block, - app.data.dynamic_resources - ); -} - -fn draw_dynamic_res_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let (title, scope) = if let Some(res) = &app.data.selected.dynamic_kind { - (res.kind.as_str(), res.scope.clone()) - } else { - ("", Scope::Cluster) - }; - let title = get_resource_title(app, title, "", app.data.dynamic_resources.items.len()); - - let (table_headers, column_widths) = if scope == Scope::Cluster { - ( - vec!["Name", "Age"], - vec![Constraint::Percentage(70), Constraint::Percentage(30)], - ) - } else { - ( - vec!["Namespace", "Name", "Age"], - vec![ - Constraint::Percentage(30), - Constraint::Percentage(50), - Constraint::Percentage(20), - ], - ) - }; - - draw_resource_block( - f, - area, - ResourceTableProps { - title, - inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), - resource: &mut app.data.dynamic_resources, - table_headers, - column_widths, - }, - |c| { - let rows = if scope == Scope::Cluster { - Row::new(vec![ - Cell::from(c.name.to_owned()), - Cell::from(c.age.to_owned()), - ]) - } else { - Row::new(vec![ - Cell::from(c.namespace.clone().unwrap_or_default()), - Cell::from(c.name.to_owned()), - Cell::from(c.age.to_owned()), - ]) - }; - rows.style(style_primary(app.light_theme)) - }, - app.light_theme, - app.is_loading, - ); -} - -/// common for all resources -fn draw_describe_block( - f: &mut Frame<'_, B>, - app: &mut App, - area: Rect, - title: Spans<'_>, -) { - let block = layout_block_top_border(title); - - let txt = &app.data.describe_out.get_txt(); - if !txt.is_empty() { - let mut txt = Text::from(txt.clone()); - txt.patch_style(style_primary(app.light_theme)); - - let paragraph = Paragraph::new(txt) - .block(block) - .wrap(Wrap { trim: false }) - .scroll((app.data.describe_out.offset, 0)); - f.render_widget(paragraph, area); - } else { - loading(f, block, area, app.is_loading, app.light_theme); - } -} - -// Utility methods - -struct ResourceTableProps<'a, T> { - title: String, - inline_help: String, - resource: &'a mut StatefulTable, - table_headers: Vec<&'a str>, - column_widths: Vec, -} - -/// Draw a kubernetes resource overview tab -fn draw_resource_block<'a, B, T, F>( - f: &mut Frame<'_, B>, - area: Rect, - table_props: ResourceTableProps<'a, T>, - row_cell_mapper: F, - light_theme: bool, - is_loading: bool, -) where - B: Backend, - F: Fn(&T) -> Row<'a>, -{ - let title = title_with_dual_style(table_props.title, table_props.inline_help, light_theme); - let block = layout_block_top_border(title); - - if !table_props.resource.items.is_empty() { - let rows = table_props - .resource - .items - .iter() - // .map(|c| { Row::new(row_cell_mapper(c)) }.style(style_primary())); - .map(row_cell_mapper); - - let table = Table::new(rows) - .header(table_header_style(table_props.table_headers, light_theme)) - .block(block) - .highlight_style(style_highlight()) - .highlight_symbol(HIGHLIGHT) - .widths(&table_props.column_widths); - - f.render_stateful_widget(table, area, &mut table_props.resource.state); - } else { - loading(f, block, area, is_loading, light_theme); - } -} - -fn get_resource_row_style(status: &str, ready: (i32, i32), light: bool) -> Style { - if status == "Running" && ready.0 == ready.1 { - style_primary(light) - } else if status == "Completed" { - style_success(light) - } else if [ - "ContainerCreating", - "PodInitializing", - "Pending", - "Initialized", - ] - .contains(&status) - { - style_secondary(light) - } else { - style_failure(light) - } -} - -fn get_cluster_wide_resource_title>(title: S, items_len: usize, suffix: S) -> String { - format!(" {} [{}] {}", title.as_ref(), items_len, suffix.as_ref()) -} - -fn get_resource_title>(app: &App, title: S, suffix: S, items_len: usize) -> String { - format!( - " {} {}", - title_with_ns( - title.as_ref(), - app - .data - .selected - .ns - .as_ref() - .unwrap_or(&String::from("all")), - items_len - ), - suffix.as_ref(), - ) -} - -fn get_container_title>(app: &App, container_len: usize, suffix: S) -> String { - let title = get_resource_title( - app, - PODS_TITLE, - format!("-> Containers [{}] {}", container_len, suffix.as_ref()).as_str(), - app.data.pods.items.len(), - ); - title -} - -fn title_with_ns(title: &str, ns: &str, length: usize) -> String { - format!("{} (ns: {}) [{}]", title, ns, length) -} - -fn get_describe_active<'a>(block: ActiveBlock) -> &'a str { - match block { - ActiveBlock::Describe => DESCRIBE_ACTIVE, - _ => YAML_ACTIVE, - } -} - #[cfg(test)] mod tests { - use tui::{backend::TestBackend, buffer::Buffer, style::Modifier, Terminal}; + use tui::{ + backend::TestBackend, + buffer::Buffer, + style::{Modifier, Style}, + Terminal, + }; use super::*; use crate::{ app::pods::KubePod, - ui::utils::{COLOR_CYAN, COLOR_RED, COLOR_WHITE, COLOR_YELLOW}, + ui::utils::{COLOR_RED, COLOR_WHITE, COLOR_YELLOW}, }; #[test] @@ -1901,160 +296,4 @@ mod tests { terminal.backend().assert_buffer(&expected); } - - #[test] - fn test_draw_resource_block() { - let backend = TestBackend::new(100, 6); - let mut terminal = Terminal::new(backend).unwrap(); - - struct RenderTest { - pub name: String, - pub namespace: String, - pub data: i32, - pub age: String, - } - terminal - .draw(|f| { - let size = f.size(); - let mut resource: StatefulTable = StatefulTable::new(); - resource.set_items(vec![ - RenderTest { - name: "Test 1".into(), - namespace: "Test ns".into(), - age: "65h3m".into(), - data: 5, - }, - RenderTest { - name: "Test long name that should be truncated from view".into(), - namespace: "Test ns".into(), - age: "65h3m".into(), - data: 3, - }, - RenderTest { - name: "test_long_name_that_should_be_truncated_from_view".into(), - namespace: "Test ns long value check that should be truncated".into(), - age: "65h3m".into(), - data: 6, - }, - ]); - draw_resource_block( - f, - size, - ResourceTableProps { - title: "Test".into(), - inline_help: "-> yaml ".into(), - resource: &mut resource, - table_headers: vec!["Namespace", "Name", "Data", "Age"], - column_widths: vec![ - Constraint::Percentage(30), - Constraint::Percentage(40), - Constraint::Percentage(15), - Constraint::Percentage(15), - ], - }, - |c| { - Row::new(vec![ - Cell::from(c.namespace.to_owned()), - Cell::from(c.name.to_owned()), - Cell::from(c.data.to_string()), - Cell::from(c.age.to_owned()), - ]) - .style(style_primary(false)) - }, - false, - false, - ); - }) - .unwrap(); - - let mut expected = Buffer::with_lines(vec![ - "Test-> yaml ─────────────────────────────────────────────────────────────────────────────────────", - " Namespace Name Data Age ", - "=> Test ns Test 1 5 65h3m ", - " Test ns Test long name that should be truncated 3 65h3m ", - " Test ns long value check that test_long_name_that_should_be_truncated_ 6 65h3m ", - " ", - ]); - // set row styles - // First row heading style - for col in 0..=99 { - match col { - 0..=3 => { - expected.get_mut(col, 0).set_style( - Style::default() - .fg(COLOR_YELLOW) - .add_modifier(Modifier::BOLD), - ); - } - 4..=14 => { - expected.get_mut(col, 0).set_style( - Style::default() - .fg(COLOR_WHITE) - .add_modifier(Modifier::BOLD), - ); - } - _ => {} - } - } - - // Second row table header style - for col in 0..=99 { - expected - .get_mut(col, 1) - .set_style(Style::default().fg(COLOR_WHITE)); - } - // first table data row style - for col in 0..=99 { - expected.get_mut(col, 2).set_style( - Style::default() - .fg(COLOR_CYAN) - .add_modifier(Modifier::REVERSED), - ); - } - // remaining table data row style - for row in 3..=4 { - for col in 0..=99 { - expected - .get_mut(col, row) - .set_style(Style::default().fg(COLOR_CYAN)); - } - } - - terminal.backend().assert_buffer(&expected); - } - - #[test] - fn test_get_resource_title() { - let app = App::default(); - assert_eq!( - get_resource_title(&app, "Title", "-> hello", 5), - " Title (ns: all) [5] -> hello" - ); - } - - #[test] - fn test_get_container_title() { - let app = App::default(); - assert_eq!( - get_container_title(&app, 3, "hello"), - " Pods (ns: all) [0] -> Containers [3] hello" - ); - } - - #[test] - fn test_title_with_ns() { - assert_eq!(title_with_ns("Title", "hello", 3), "Title (ns: hello) [3]"); - } - - #[test] - fn test_get_cluster_wide_resource_title() { - assert_eq!( - get_cluster_wide_resource_title("Cluster Resource", 3, ""), - " Cluster Resource [3] " - ); - assert_eq!( - get_cluster_wide_resource_title("Nodes", 10, "-> hello"), - " Nodes [10] -> hello" - ); - } } diff --git a/src/ui/utilization.rs b/src/ui/utilization.rs deleted file mode 100644 index 6908e542..00000000 --- a/src/ui/utilization.rs +++ /dev/null @@ -1,108 +0,0 @@ -use kubectl_view_allocations::{qty::Qty, tree::provide_prefix}; -use tui::{ - backend::Backend, - layout::{Constraint, Rect}, - widgets::{Cell, Row, Table}, - Frame, -}; - -use super::utils::{ - layout_block_active, loading, style_highlight, style_primary, style_success, style_warning, - table_header_style, -}; -use crate::app::App; - -pub fn draw_utilization(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { - let title = format!( - " Resource Utilization (ns: [{}], group by : {:?}) ", - app - .data - .selected - .ns - .as_ref() - .unwrap_or(&String::from("all")), - app.utilization_group_by - ); - let block = layout_block_active(title.as_str(), app.light_theme); - - if !app.data.metrics.items.is_empty() { - let data = &app.data.metrics.items; - - let prefixes = provide_prefix(data, |parent, item| parent.0.len() + 1 == item.0.len()); - - // Create the table - let mut rows: Vec> = vec![]; - for ((k, oqtys), prefix) in data.iter().zip(prefixes.iter()) { - let column0 = format!( - "{} {}", - prefix, - k.last().map(|x| x.as_str()).unwrap_or("???") - ); - if let Some(qtys) = oqtys { - let style = if qtys.requested > qtys.limit || qtys.utilization > qtys.limit { - style_warning(app.light_theme) - } else if is_empty(&qtys.requested) || is_empty(&qtys.limit) { - style_primary(app.light_theme) - } else { - style_success(app.light_theme) - }; - - let row = Row::new(vec![ - Cell::from(column0), - make_table_cell(&qtys.utilization, &qtys.allocatable), - make_table_cell(&qtys.requested, &qtys.allocatable), - make_table_cell(&qtys.limit, &qtys.allocatable), - make_table_cell(&qtys.allocatable, &None), - make_table_cell(&qtys.calc_free(), &None), - ]) - .style(style); - rows.push(row); - } - } - - let table = Table::new(rows) - .header(table_header_style( - vec![ - "Resource", - "Utilization", - "Requested", - "Limit", - "Allocatable", - "Free", - ], - app.light_theme, - )) - .block(block) - .widths(&[ - Constraint::Percentage(50), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(10), - ]) - .highlight_style(style_highlight()); - - f.render_stateful_widget(table, area, &mut app.data.metrics.state); - } else { - loading(f, block, area, app.is_loading, app.light_theme); - } -} - -fn make_table_cell<'a>(oqty: &Option, o100: &Option) -> Cell<'a> { - let txt = match oqty { - None => "__".into(), - Some(ref qty) => match o100 { - None => format!("{}", qty.adjust_scale()), - Some(q100) => format!("{} ({:.0}%)", qty.adjust_scale(), qty.calc_percentage(q100)), - }, - }; - Cell::from(txt) -} - -fn is_empty(oqty: &Option) -> bool { - match oqty { - Some(qty) => qty.is_zero(), - None => true, - } -} diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 056fb909..4d94bca2 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -6,11 +6,21 @@ use tui::{ style::{Color, Modifier, Style}, symbols, text::{Span, Spans, Text}, - widgets::{Block, Borders, Paragraph, Row}, + widgets::{Block, Borders, Paragraph, Row, Table, Wrap}, Frame, }; + +use crate::app::{models::StatefulTable, ActiveBlock, App}; + +use super::HIGHLIGHT; // Utils +pub static COPY_HINT: &str = "| copy "; +pub static DESCRIBE_AND_YAML_HINT: &str = "| describe | yaml "; +pub static DESCRIBE_YAML_AND_ESC_HINT: &str = "| describe | yaml | back to menu "; +pub static DESCRIBE_YAML_DECODE_AND_ESC_HINT: &str = + "| describe | yaml | decode | back to menu "; + // default colors pub const COLOR_TEAL: Color = Color::Rgb(35, 50, 55); pub const COLOR_CYAN: Color = Color::Rgb(0, 230, 230); @@ -272,3 +282,292 @@ pub fn loading( f.render_widget(block, area) } } + +// using a macro to reuse code as generics will make handling lifetimes a PITA +#[macro_export] +macro_rules! draw_resource_tab { + ($title:expr, $block:expr, $f:expr, $app:expr, $area:expr, $fn1:expr, $fn2:expr, $res:expr) => { + match $block { + ActiveBlock::Describe | ActiveBlock::Yaml => draw_describe_block( + $f, + $app, + $area, + title_with_dual_style( + get_resource_title($app, $title, get_describe_active($block), $res.items.len()), + format!("{} | {} ", COPY_HINT, $title), + $app.light_theme, + ), + ), + ActiveBlock::Namespaces => $fn1($app.get_prev_route().active_block, $f, $app, $area), + _ => $fn2($f, $app, $area), + }; + }; +} + +pub struct ResourceTableProps<'a, T> { + pub title: String, + pub inline_help: String, + pub resource: &'a mut StatefulTable, + pub table_headers: Vec<&'a str>, + pub column_widths: Vec, +} +/// common for all resources +pub fn draw_describe_block( + f: &mut Frame<'_, B>, + app: &mut App, + area: Rect, + title: Spans<'_>, +) { + let block = layout_block_top_border(title); + + let txt = &app.data.describe_out.get_txt(); + if !txt.is_empty() { + let mut txt = Text::from(txt.clone()); + txt.patch_style(style_primary(app.light_theme)); + + let paragraph = Paragraph::new(txt) + .block(block) + .wrap(Wrap { trim: false }) + .scroll((app.data.describe_out.offset, 0)); + f.render_widget(paragraph, area); + } else { + loading(f, block, area, app.is_loading, app.light_theme); + } +} + +/// Draw a kubernetes resource overview tab +pub fn draw_resource_block<'a, B, T, F>( + f: &mut Frame<'_, B>, + area: Rect, + table_props: ResourceTableProps<'a, T>, + row_cell_mapper: F, + light_theme: bool, + is_loading: bool, +) where + B: Backend, + F: Fn(&T) -> Row<'a>, +{ + let title = title_with_dual_style(table_props.title, table_props.inline_help, light_theme); + let block = layout_block_top_border(title); + + if !table_props.resource.items.is_empty() { + let rows = table_props + .resource + .items + .iter() + // .map(|c| { Row::new(row_cell_mapper(c)) }.style(style_primary())); + .map(row_cell_mapper); + + let table = Table::new(rows) + .header(table_header_style(table_props.table_headers, light_theme)) + .block(block) + .highlight_style(style_highlight()) + .highlight_symbol(HIGHLIGHT) + .widths(&table_props.column_widths); + + f.render_stateful_widget(table, area, &mut table_props.resource.state); + } else { + loading(f, block, area, is_loading, light_theme); + } +} + +pub fn get_cluster_wide_resource_title>( + title: S, + items_len: usize, + suffix: S, +) -> String { + format!(" {} [{}] {}", title.as_ref(), items_len, suffix.as_ref()) +} + +pub fn get_resource_title>( + app: &App, + title: S, + suffix: S, + items_len: usize, +) -> String { + format!( + " {} {}", + title_with_ns( + title.as_ref(), + app + .data + .selected + .ns + .as_ref() + .unwrap_or(&String::from("all")), + items_len + ), + suffix.as_ref(), + ) +} + +static DESCRIBE_ACTIVE: &str = "-> Describe "; +static YAML_ACTIVE: &str = "-> YAML "; + +pub fn get_describe_active<'a>(block: ActiveBlock) -> &'a str { + match block { + ActiveBlock::Describe => DESCRIBE_ACTIVE, + _ => YAML_ACTIVE, + } +} + +pub fn title_with_ns(title: &str, ns: &str, length: usize) -> String { + format!("{} (ns: {}) [{}]", title, ns, length) +} + +#[cfg(test)] +mod tests { + use tui::{backend::TestBackend, buffer::Buffer, style::Modifier, widgets::Cell, Terminal}; + + use super::*; + use crate::{ + ui::utils::{COLOR_CYAN, COLOR_WHITE, COLOR_YELLOW}, + }; + + #[test] + fn test_draw_resource_block() { + let backend = TestBackend::new(100, 6); + let mut terminal = Terminal::new(backend).unwrap(); + + struct RenderTest { + pub name: String, + pub namespace: String, + pub data: i32, + pub age: String, + } + terminal + .draw(|f| { + let size = f.size(); + let mut resource: StatefulTable = StatefulTable::new(); + resource.set_items(vec![ + RenderTest { + name: "Test 1".into(), + namespace: "Test ns".into(), + age: "65h3m".into(), + data: 5, + }, + RenderTest { + name: "Test long name that should be truncated from view".into(), + namespace: "Test ns".into(), + age: "65h3m".into(), + data: 3, + }, + RenderTest { + name: "test_long_name_that_should_be_truncated_from_view".into(), + namespace: "Test ns long value check that should be truncated".into(), + age: "65h3m".into(), + data: 6, + }, + ]); + draw_resource_block( + f, + size, + ResourceTableProps { + title: "Test".into(), + inline_help: "-> yaml ".into(), + resource: &mut resource, + table_headers: vec!["Namespace", "Name", "Data", "Age"], + column_widths: vec![ + Constraint::Percentage(30), + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(15), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.namespace.to_owned()), + Cell::from(c.name.to_owned()), + Cell::from(c.data.to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(false)) + }, + false, + false, + ); + }) + .unwrap(); + + let mut expected = Buffer::with_lines(vec![ + "Test-> yaml ─────────────────────────────────────────────────────────────────────────────────────", + " Namespace Name Data Age ", + "=> Test ns Test 1 5 65h3m ", + " Test ns Test long name that should be truncated 3 65h3m ", + " Test ns long value check that test_long_name_that_should_be_truncated_ 6 65h3m ", + " ", + ]); + // set row styles + // First row heading style + for col in 0..=99 { + match col { + 0..=3 => { + expected.get_mut(col, 0).set_style( + Style::default() + .fg(COLOR_YELLOW) + .add_modifier(Modifier::BOLD), + ); + } + 4..=14 => { + expected.get_mut(col, 0).set_style( + Style::default() + .fg(COLOR_WHITE) + .add_modifier(Modifier::BOLD), + ); + } + _ => {} + } + } + + // Second row table header style + for col in 0..=99 { + expected + .get_mut(col, 1) + .set_style(Style::default().fg(COLOR_WHITE)); + } + // first table data row style + for col in 0..=99 { + expected.get_mut(col, 2).set_style( + Style::default() + .fg(COLOR_CYAN) + .add_modifier(Modifier::REVERSED), + ); + } + // remaining table data row style + for row in 3..=4 { + for col in 0..=99 { + expected + .get_mut(col, row) + .set_style(Style::default().fg(COLOR_CYAN)); + } + } + + terminal.backend().assert_buffer(&expected); + } + + #[test] + fn test_get_resource_title() { + let app = App::default(); + assert_eq!( + get_resource_title(&app, "Title", "-> hello", 5), + " Title (ns: all) [5] -> hello" + ); + } + + #[test] + fn test_title_with_ns() { + assert_eq!(title_with_ns("Title", "hello", 3), "Title (ns: hello) [3]"); + } + + #[test] + fn test_get_cluster_wide_resource_title() { + assert_eq!( + get_cluster_wide_resource_title("Cluster Resource", 3, ""), + " Cluster Resource [3] " + ); + assert_eq!( + get_cluster_wide_resource_title("Nodes", 10, "-> hello"), + " Nodes [10] -> hello" + ); + } +} From d54ec490b31baef92ac1a3b0ea0c182bd616db28 Mon Sep 17 00:00:00 2001 From: Deepu Date: Tue, 22 Aug 2023 09:29:25 +0200 Subject: [PATCH 2/3] lint --- Makefile | 2 +- src/app/storageclass.rs | 3 ++- src/ui/utils.rs | 4 +--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 1e1f634c..9c42c1d9 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ lint: ## Fix lint lint-fix: - @cargo fix + @cargo fix --allow-staged ## Run format fmt: diff --git a/src/app/storageclass.rs b/src/app/storageclass.rs index 9ffcef41..9714634b 100644 --- a/src/app/storageclass.rs +++ b/src/app/storageclass.rs @@ -18,7 +18,8 @@ use crate::{ network::Network, ui::utils::{ draw_describe_block, draw_resource_block, get_cluster_wide_resource_title, get_describe_active, - get_resource_title, style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, DESCRIBE_YAML_AND_ESC_HINT, + get_resource_title, style_primary, title_with_dual_style, ResourceTableProps, COPY_HINT, + DESCRIBE_YAML_AND_ESC_HINT, }, }; diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 4d94bca2..519d0e75 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -420,9 +420,7 @@ mod tests { use tui::{backend::TestBackend, buffer::Buffer, style::Modifier, widgets::Cell, Terminal}; use super::*; - use crate::{ - ui::utils::{COLOR_CYAN, COLOR_WHITE, COLOR_YELLOW}, - }; + use crate::ui::utils::{COLOR_CYAN, COLOR_WHITE, COLOR_YELLOW}; #[test] fn test_draw_resource_block() { From 3854d08975acbd4989eb7d12f59862fca91affbc Mon Sep 17 00:00:00 2001 From: Deepu Date: Tue, 22 Aug 2023 09:53:58 +0200 Subject: [PATCH 3/3] rename methods --- src/app/cronjobs.rs | 4 ++-- src/app/daemonsets.rs | 4 ++-- src/app/deployments.rs | 4 ++-- src/app/dynamic.rs | 4 ++-- src/app/ingress.rs | 4 ++-- src/app/jobs.rs | 4 ++-- src/app/network_policies.rs | 4 ++-- src/app/nodes.rs | 4 ++-- src/app/pods.rs | 4 ++-- src/app/pvcs.rs | 4 ++-- src/app/pvs.rs | 4 ++-- src/app/replicasets.rs | 4 ++-- src/app/replication_controllers.rs | 4 ++-- src/app/secrets.rs | 4 ++-- src/app/serviceaccounts.rs | 4 ++-- src/app/statefulsets.rs | 4 ++-- src/app/storageclass.rs | 4 ++-- src/app/svcs.rs | 4 ++-- 18 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/app/cronjobs.rs b/src/app/cronjobs.rs index 139825de..9c8c5f53 100644 --- a/src/app/cronjobs.rs +++ b/src/app/cronjobs.rs @@ -82,7 +82,7 @@ impl AppResource for CronJobResource { app, area, Self::render, - draw_cronjobs_block, + draw_block, app.data.cronjobs ); } @@ -95,7 +95,7 @@ impl AppResource for CronJobResource { } } -fn draw_cronjobs_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, CRON_JOBS_TITLE, "", app.data.cronjobs.items.len()); draw_resource_block( diff --git a/src/app/daemonsets.rs b/src/app/daemonsets.rs index 0af78560..0e7417bb 100644 --- a/src/app/daemonsets.rs +++ b/src/app/daemonsets.rs @@ -80,7 +80,7 @@ impl AppResource for DaemonSetResource { app, area, Self::render, - draw_daemon_sets_block, + draw_block, app.data.daemon_sets ); } @@ -93,7 +93,7 @@ impl AppResource for DaemonSetResource { } } -fn draw_daemon_sets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, DAEMON_SETS_TITLE, "", app.data.daemon_sets.items.len()); draw_resource_block( diff --git a/src/app/deployments.rs b/src/app/deployments.rs index c30788e2..61b94c33 100644 --- a/src/app/deployments.rs +++ b/src/app/deployments.rs @@ -79,7 +79,7 @@ impl AppResource for DeploymentResource { app, area, Self::render, - draw_deployments_block, + draw_block, app.data.deployments ); } @@ -92,7 +92,7 @@ impl AppResource for DeploymentResource { } } -fn draw_deployments_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, DEPLOYMENTS_TITLE, "", app.data.deployments.items.len()); draw_resource_block( diff --git a/src/app/dynamic.rs b/src/app/dynamic.rs index 393556a3..bc5b17f4 100644 --- a/src/app/dynamic.rs +++ b/src/app/dynamic.rs @@ -95,7 +95,7 @@ impl AppResource for DynamicResource { app, area, Self::render, - draw_dynamic_res_block, + draw_block, app.data.dynamic_resources ); } @@ -131,7 +131,7 @@ impl AppResource for DynamicResource { } } -fn draw_dynamic_res_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let (title, scope) = if let Some(res) = &app.data.selected.dynamic_kind { (res.kind.as_str(), res.scope.clone()) } else { diff --git a/src/app/ingress.rs b/src/app/ingress.rs index 7afbdae0..2261e2db 100644 --- a/src/app/ingress.rs +++ b/src/app/ingress.rs @@ -174,7 +174,7 @@ impl AppResource for IngressResource { app, area, Self::render, - draw_ingress_block, + draw_block, app.data.ingress ); } @@ -187,7 +187,7 @@ impl AppResource for IngressResource { } } -fn draw_ingress_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, INGRESS_TITLE, "", app.data.ingress.items.len()); draw_resource_block( diff --git a/src/app/jobs.rs b/src/app/jobs.rs index 64aa1c2c..1f3e394e 100644 --- a/src/app/jobs.rs +++ b/src/app/jobs.rs @@ -90,7 +90,7 @@ impl AppResource for JobResource { app, area, Self::render, - draw_jobs_block, + draw_block, app.data.jobs ); } @@ -103,7 +103,7 @@ impl AppResource for JobResource { } } -fn draw_jobs_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, JOBS_TITLE, "", app.data.jobs.items.len()); draw_resource_block( diff --git a/src/app/network_policies.rs b/src/app/network_policies.rs index 664476cb..4e50e009 100644 --- a/src/app/network_policies.rs +++ b/src/app/network_policies.rs @@ -83,7 +83,7 @@ impl AppResource for NetworkPolicyResource { app, area, Self::render, - draw_nw_policy_block, + draw_block, app.data.nw_policies ); } @@ -96,7 +96,7 @@ impl AppResource for NetworkPolicyResource { } } -fn draw_nw_policy_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, NW_POLICY_TITLE, "", app.data.nw_policies.items.len()); draw_resource_block( diff --git a/src/app/nodes.rs b/src/app/nodes.rs index 5701081b..777bf9dc 100644 --- a/src/app/nodes.rs +++ b/src/app/nodes.rs @@ -203,7 +203,7 @@ impl AppResource for NodeResource { ), ), ActiveBlock::Namespaces => Self::render(app.get_prev_route().active_block, f, app, area), - _ => draw_nodes_block(f, app, area), + _ => draw_block(f, app, area), }; } @@ -264,7 +264,7 @@ async fn get_node_metrics(nw: &Network<'_>) { }; } -fn draw_nodes_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_cluster_wide_resource_title(NODES_TITLE, app.data.nodes.items.len(), ""); draw_resource_block( diff --git a/src/app/pods.rs b/src/app/pods.rs index b35359bb..3f82d4f4 100644 --- a/src/app/pods.rs +++ b/src/app/pods.rs @@ -169,7 +169,7 @@ impl AppResource for PodResource { ), ActiveBlock::Logs => draw_logs_block(f, app, area), ActiveBlock::Namespaces => Self::render(app.get_prev_route().active_block, f, app, area), - _ => draw_pods_block(f, app, area), + _ => draw_block(f, app, area), } } @@ -193,7 +193,7 @@ impl AppResource for PodResource { } } -fn draw_pods_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, PODS_TITLE, "", app.data.pods.items.len()); draw_resource_block( diff --git a/src/app/pvcs.rs b/src/app/pvcs.rs index 4c2156eb..e08d2721 100644 --- a/src/app/pvcs.rs +++ b/src/app/pvcs.rs @@ -104,7 +104,7 @@ impl AppResource for PvcResource { app, area, Self::render, - draw_pvc_block, + draw_block, app.data.pvcs ); } @@ -119,7 +119,7 @@ impl AppResource for PvcResource { } } -fn draw_pvc_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, PVC_TITLE, "", app.data.pvcs.items.len()); draw_resource_block( diff --git a/src/app/pvs.rs b/src/app/pvs.rs index 616ea736..e124b3f6 100644 --- a/src/app/pvs.rs +++ b/src/app/pvs.rs @@ -123,7 +123,7 @@ impl AppResource for PvResource { app, area, Self::render, - draw_pv_block, + draw_block, app.data.pvs ); } @@ -136,7 +136,7 @@ impl AppResource for PvResource { } } -fn draw_pv_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, PV_TITLE, "", app.data.pvs.items.len()); draw_resource_block( diff --git a/src/app/replicasets.rs b/src/app/replicasets.rs index fac49e49..1a10d411 100644 --- a/src/app/replicasets.rs +++ b/src/app/replicasets.rs @@ -75,7 +75,7 @@ impl AppResource for ReplicaSetResource { app, area, Self::render, - draw_replica_sets_block, + draw_block, app.data.replica_sets ); } @@ -88,7 +88,7 @@ impl AppResource for ReplicaSetResource { } } -fn draw_replica_sets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title( app, REPLICA_SETS_TITLE, diff --git a/src/app/replication_controllers.rs b/src/app/replication_controllers.rs index f9f2b285..12efe799 100644 --- a/src/app/replication_controllers.rs +++ b/src/app/replication_controllers.rs @@ -116,7 +116,7 @@ impl AppResource for ReplicationControllerResource { app, area, Self::render, - draw_replication_controllers_block, + draw_block, app.data.rpl_ctrls ); } @@ -131,7 +131,7 @@ impl AppResource for ReplicationControllerResource { } } -fn draw_replication_controllers_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, RPL_CTRL_TITLE, "", app.data.rpl_ctrls.items.len()); draw_resource_block( diff --git a/src/app/secrets.rs b/src/app/secrets.rs index 3893774d..69062d64 100644 --- a/src/app/secrets.rs +++ b/src/app/secrets.rs @@ -68,7 +68,7 @@ impl AppResource for SecretResource { app, area, Self::render, - draw_secrets_block, + draw_block, app.data.secrets ); } @@ -81,7 +81,7 @@ impl AppResource for SecretResource { } } -fn draw_secrets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, SECRETS_TITLE, "", app.data.secrets.items.len()); draw_resource_block( diff --git a/src/app/serviceaccounts.rs b/src/app/serviceaccounts.rs index 531ab4ef..14f2b62e 100644 --- a/src/app/serviceaccounts.rs +++ b/src/app/serviceaccounts.rs @@ -65,7 +65,7 @@ impl AppResource for SvcAcctResource { app, area, Self::render, - draw_svc_acct_block, + draw_block, app.data.service_accounts ); } @@ -78,7 +78,7 @@ impl AppResource for SvcAcctResource { } } -fn draw_svc_acct_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title( app, SVC_ACCT_TITLE, diff --git a/src/app/statefulsets.rs b/src/app/statefulsets.rs index ec7a484d..cab9d800 100644 --- a/src/app/statefulsets.rs +++ b/src/app/statefulsets.rs @@ -73,7 +73,7 @@ impl AppResource for StatefulSetResource { app, area, Self::render, - draw_stateful_sets_block, + draw_block, app.data.stateful_sets ); } @@ -86,7 +86,7 @@ impl AppResource for StatefulSetResource { } } -fn draw_stateful_sets_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, STFS_TITLE, "", app.data.stateful_sets.items.len()); draw_resource_block( diff --git a/src/app/storageclass.rs b/src/app/storageclass.rs index 9714634b..fb0f7894 100644 --- a/src/app/storageclass.rs +++ b/src/app/storageclass.rs @@ -74,7 +74,7 @@ impl AppResource for StorageClassResource { app, area, Self::render, - draw_storage_classes_block, + draw_block, app.data.storage_classes ); } @@ -87,7 +87,7 @@ impl AppResource for StorageClassResource { } } -fn draw_storage_classes_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_cluster_wide_resource_title( STORAGE_CLASSES_LABEL, app.data.storage_classes.items.len(), diff --git a/src/app/svcs.rs b/src/app/svcs.rs index a326799e..fad75587 100644 --- a/src/app/svcs.rs +++ b/src/app/svcs.rs @@ -101,7 +101,7 @@ impl AppResource for SvcResource { app, area, Self::render, - draw_services_block, + draw_block, app.data.services ); } @@ -113,7 +113,7 @@ impl AppResource for SvcResource { } } -fn draw_services_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { +fn draw_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let title = get_resource_title(app, SERVICES_TITLE, "", app.data.services.items.len()); draw_resource_block(