diff --git a/opendut-lea/opendut-lea-components/src/health.rs b/opendut-lea/opendut-lea-components/src/health.rs index e239fb68e..b12f93914 100644 --- a/opendut-lea/opendut-lea-components/src/health.rs +++ b/opendut-lea/opendut-lea-components/src/health.rs @@ -1,5 +1,5 @@ use leptos::prelude::*; -use crate::tooltip::Tooltip; +use crate::tooltip::{Tooltip, TooltipDirection}; pub struct State { pub kind: StateKind, @@ -15,9 +15,12 @@ pub enum StateKind { } #[component] -pub fn Health(state: Signal) -> impl IntoView { +pub fn Health( + state: Signal, + #[prop(optional)] tooltip_direction: TooltipDirection, +) -> impl IntoView { - let classes = move || state.with(|state| { + let health_class = move || state.with(|state| { match state.kind { StateKind::Unknown => "health-light", StateKind::Red => "health-light red", @@ -26,14 +29,21 @@ pub fn Health(state: Signal) -> impl IntoView { } }); - let tool_tip_text = Signal::derive(move || state.with(|state| { + let tooltip_text = Signal::derive(move || state.with(|state| { Clone::clone(&state.text) })); + + let tooltip_content = Box::new(move || { + view! { +

{ tooltip_text }

+ + }.into_any() + }); view! {
- -
+ +
} diff --git a/opendut-lea/opendut-lea-components/src/tooltip.rs b/opendut-lea/opendut-lea-components/src/tooltip.rs index ac589b05f..6e7121b97 100644 --- a/opendut-lea/opendut-lea-components/src/tooltip.rs +++ b/opendut-lea/opendut-lea-components/src/tooltip.rs @@ -1,6 +1,8 @@ use leptos::prelude::*; +#[derive(Default, Clone, Copy)] pub enum TooltipDirection { + #[default] Left, Right, Up, @@ -18,10 +20,12 @@ impl TooltipDirection { } } +pub type TooltipContent = Box AnyView + Send>; + #[component] pub fn Tooltip( - #[prop(into)] text: Signal, - #[prop(into, default=Signal::from(TooltipDirection::Left))] direction: Signal, + text: TooltipContent, + #[prop(into, optional)] direction: Signal, #[prop(into, default=Signal::from(false))] is_hidden: Signal, children: Children ) -> impl IntoView { @@ -34,7 +38,7 @@ pub fn Tooltip(
- { text } + { text() }
diff --git a/opendut-lea/src/clusters/components/cluster_health.rs b/opendut-lea/src/clusters/components/cluster_health.rs index 371606ec4..25324a552 100644 --- a/opendut-lea/src/clusters/components/cluster_health.rs +++ b/opendut-lea/src/clusters/components/cluster_health.rs @@ -1,10 +1,12 @@ use leptos::prelude::*; use opendut_lea_components::health::{self, Health}; +use opendut_lea_components::tooltip::TooltipDirection; use opendut_model::cluster::state::ClusterState; #[component] pub fn ClusterHealth( - #[prop(into)] state: Signal + #[prop(into)] state: Signal, + #[prop(optional)] tooltip_direction: TooltipDirection ) -> impl IntoView { let _ = state; @@ -17,6 +19,9 @@ pub fn ClusterHealth( }); view! { - + } } diff --git a/opendut-lea/src/clusters/components/delete_cluster_button.rs b/opendut-lea/src/clusters/components/delete_cluster_button.rs index 87f4bda48..4cfa3828e 100644 --- a/opendut-lea/src/clusters/components/delete_cluster_button.rs +++ b/opendut-lea/src/clusters/components/delete_cluster_button.rs @@ -69,9 +69,15 @@ where F: Fn() + Clone + Send + 'static { !matches!(button_state.get(), ButtonState::Disabled) }); + let tooltip_content = Box::new(move || { + view! { + Cluster can not be deleted while it is deployed. + }.into_any() + }); + view! { diff --git a/opendut-lea/src/clusters/components/deploy_toggle.rs b/opendut-lea/src/clusters/components/deploy_toggle.rs index dff92871d..fb85e1a35 100644 --- a/opendut-lea/src/clusters/components/deploy_toggle.rs +++ b/opendut-lea/src/clusters/components/deploy_toggle.rs @@ -3,11 +3,12 @@ use std::sync::Arc; use leptos::prelude::*; use tracing::{debug, error}; use opendut_carl_api::carl::ClientError; -use opendut_carl_api::carl::cluster::StoreClusterDeploymentError; -use opendut_lea_components::tooltip::Tooltip; +use opendut_carl_api::carl::cluster::{ListClusterPeerStatesResponse, StoreClusterDeploymentError}; +use opendut_lea_components::tooltip::{Tooltip, TooltipDirection}; use opendut_lea_components::{Toggle, ToggleState}; use opendut_model::cluster::{ClusterDeployment, ClusterId}; - +use opendut_model::peer::PeerId; +use opendut_model::peer::state::PeerMemberState; use crate::app::use_app_globals; use crate::clusters::IsDeployed; use crate::components::{Toast, use_toaster}; @@ -15,8 +16,10 @@ use crate::components::{Toast, use_toaster}; #[component] pub fn DeployToggle( #[prop(into)] cluster_id: Signal, + #[prop(into, default=Signal::from(false))] is_new_cluster: Signal, #[prop(into)] is_deployed: Signal, on_deployment_changed: OnDeploymentChanged, + #[prop(optional)] tooltip_direction: TooltipDirection, ) -> impl IntoView where OnDeploymentChanged: Fn() + Clone + Send + 'static, @@ -101,23 +104,152 @@ where } }; - let tooltip_text = Signal::derive(move || { - if is_deployed.get().0 { - "Deployment requested".to_string() + let blocked_cluster_deployment = { + let carl = globals.client.clone(); + let cluster_id = cluster_id.get_untracked(); + + LocalResource::new(move || { + let mut carl = carl.clone(); + let is_new_cluster = is_new_cluster.get(); + let _ = is_deployed.get(); + + async move { + if is_new_cluster { + return None; + } + + let states = carl.cluster.list_cluster_peer_states(cluster_id).await + .expect("Failed to request the list of cluster's peer state."); + match states { + ListClusterPeerStatesResponse::Success { peer_states } => { + let invalid_peers = peer_states.iter() + .filter(|(_, peer_state)| { + matches!(&peer_state.member, PeerMemberState::Blocked { .. }) + }) + .map(|(peer_id, _)| peer_id.to_owned()) + .collect::>(); + + if invalid_peers.is_empty() { + None + } else { + Some(BlockedDeployment::Peers(invalid_peers)) + } + } + ListClusterPeerStatesResponse::Failure { message } => { + Some(BlockedDeployment::Message(message)) + } + } + } + }) + }; + + move || { + let on_deploy = on_deploy.clone(); + let on_undeploy = on_undeploy.clone(); + + Suspend::new(async move { + let blocked_deployment = blocked_cluster_deployment.await; + + let is_deployed = Signal::derive(move || is_deployed.get().0); + + let show_error = Signal::derive({ + let blocked_deployment = blocked_deployment.clone(); + move || blocked_deployment.is_some() && !is_deployed.get() + }); + + let toggle_state = Signal::derive(move || { + if show_error.get() || is_new_cluster.get() { + ToggleState::Disabled + } else { + ToggleState::Enabled + } + }); + + let tooltip_content = Box::new(move || { + view! { + + }.into_any() + }); + + view! { + + + + } + }) + } +} + +#[derive(Clone)] +enum BlockedDeployment { + Peers(Vec), + Message(String), +} + +#[component] +fn DeploymentTooltipContent( + is_new_cluster: Signal, + is_deployed: Signal, + blocked_deployment: Option, +) -> impl IntoView { + + move || { + if is_new_cluster.get() { + return view! { + "Save the cluster first." + }.into_any(); + } + + if !is_deployed.get() + && let Some(blocked_deployment) = blocked_deployment.as_ref() { + return match blocked_deployment { + BlockedDeployment::Peers(peers) => { + let amount = peers.len(); + + let message = if amount == 1 { + "1 Peer is already in use:".to_string() + } else { + format!("{amount} Peers are already in use:") + }; + + let peer_links = peers + .iter() + .enumerate() + .map(|(index, peer_id)| { + let href = format!("/peers/{peer_id}/configure/general"); + + view! { + + {if index == 0 { "" } else { ", " }} + {peer_id.to_string()} + + } + }) + .collect_view(); + + view! { + {message} " " {peer_links} + } + .into_any() + } + + BlockedDeployment::Message(message) => { + view! { { message.to_owned() } }.into_any() + } + }; + } + + + if is_deployed.get() { + view! { "Deployment requested" }.into_any() } else { - "Undeployed".to_string() + view! { "Undeployed" }.into_any() } - }); - - view! { - - - } } diff --git a/opendut-lea/src/clusters/configurator/components/controls.rs b/opendut-lea/src/clusters/configurator/components/controls.rs index 1ef03b955..b4a0bd9db 100644 --- a/opendut-lea/src/clusters/configurator/components/controls.rs +++ b/opendut-lea/src/clusters/configurator/components/controls.rs @@ -37,20 +37,22 @@ where navigate_to(WellKnownRoutes::ClustersOverview, use_navigate.clone()); } }; + + let tooltip_direction = TooltipDirection::Right; view! {
-
+
-
-
-
- +
+
diff --git a/opendut-lea/src/peers/components/delete_peer_button.rs b/opendut-lea/src/peers/components/delete_peer_button.rs index fde7e3b09..9c1a2111c 100644 --- a/opendut-lea/src/peers/components/delete_peer_button.rs +++ b/opendut-lea/src/peers/components/delete_peer_button.rs @@ -69,9 +69,15 @@ where F: Fn() + Clone + Send + 'static { !matches!(button_state.get(), ButtonState::Disabled) }); + let tooltip_content = Box::new(move || { + view! { + Peer can not be deleted while it is configured in a cluster. + }.into_any() + }); + view! { diff --git a/opendut-lea/src/peers/configurator/tabs/devices/device_panel.rs b/opendut-lea/src/peers/configurator/tabs/devices/device_panel.rs index f08a5bc3d..dce03fea4 100644 --- a/opendut-lea/src/peers/configurator/tabs/devices/device_panel.rs +++ b/opendut-lea/src/peers/configurator/tabs/devices/device_panel.rs @@ -34,7 +34,6 @@ where -
diff --git a/opendut-lea/src/viper_tests/components/duplicate_viper_test_button.rs b/opendut-lea/src/viper_tests/components/duplicate_viper_test_button.rs index 66bebaea4..b56aa1334 100644 --- a/opendut-lea/src/viper_tests/components/duplicate_viper_test_button.rs +++ b/opendut-lea/src/viper_tests/components/duplicate_viper_test_button.rs @@ -63,8 +63,14 @@ pub fn DuplicateViperTestButton( }) }; + let tooltip_content = Box::new(move || { + view! { + Duplicate Test Run. + }.into_any() + }); + view! { - +