diff --git a/Cargo.lock b/Cargo.lock index b9a8342d..1a476e91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11591,6 +11591,7 @@ dependencies = [ "subspace-runtime-primitives", "subspace-service", "supports-color", + "tempfile", "thiserror", "thread-priority", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 39346135..7c77aba1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ subspace-rpc-primitives = { git = "https://github.com/subspace/subspace", rev = subspace-runtime-primitives = { git = "https://github.com/subspace/subspace", rev = "18dd43ab3ca9666aec256fb99c6cb7b8e4eeaea5" } subspace-service = { git = "https://github.com/subspace/subspace", rev = "18dd43ab3ca9666aec256fb99c6cb7b8e4eeaea5" } supports-color = "3.0.0" +tempfile = "3.10.1" thiserror = "1.0.61" thread-priority = "1.1.0" tokio = { version = "1.38.0", features = ["fs", "time"] } diff --git a/src/backend.rs b/src/backend.rs index 4c016417..e290ef26 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -185,16 +185,14 @@ pub enum BackendNotification { /// Progress in %: 0.0..=100.0 progress: f32, }, - IncompatibleChain { + ConfigurationFound { raw_config: RawConfig, + }, + IncompatibleChain { compatible_chain: String, }, NotConfigured, - // TODO: Indicate what is invalid so that UI can render it properly ConfigurationIsInvalid { - // TODO: Remove suppression once used - #[allow(dead_code)] - config: RawConfig, error: ConfigError, }, ConfigSaveResult(anyhow::Result<()>), @@ -239,10 +237,7 @@ struct LoadedBackend { enum BackendLoadingResult { Success(LoadedBackend), - IncompatibleChain { - raw_config: RawConfig, - compatible_chain: String, - }, + IncompatibleChain { compatible_chain: String }, } // NOTE: this is an async function, but it might do blocking operations and should be running on a @@ -272,10 +267,7 @@ pub async fn create( BackendAction::NewConfig { raw_config } => { if let Err(error) = Config::try_from_raw_config(&raw_config).await { notifications_sender - .send(BackendNotification::ConfigurationIsInvalid { - config: raw_config.clone(), - error, - }) + .send(BackendNotification::ConfigurationIsInvalid { error }) .await?; } @@ -312,15 +304,9 @@ pub async fn create( // Loaded successfully loaded_backend } - Ok(BackendLoadingResult::IncompatibleChain { - raw_config, - compatible_chain, - }) => { + Ok(BackendLoadingResult::IncompatibleChain { compatible_chain }) => { if let Err(error) = notifications_sender - .send(BackendNotification::IncompatibleChain { - raw_config, - compatible_chain, - }) + .send(BackendNotification::IncompatibleChain { compatible_chain }) .await { error!(%error, "Failed to send incompatible chain notification"); @@ -424,7 +410,6 @@ async fn load( LoadedConsensusChainNode::Compatible(consensus_node) => consensus_node, LoadedConsensusChainNode::Incompatible { compatible_chain } => { return Ok(Some(BackendLoadingResult::IncompatibleChain { - raw_config, compatible_chain, })); } @@ -617,24 +602,23 @@ async fn load_configuration( }) .await?; - // TODO: Make configuration errors recoverable - let maybe_config = RawConfig::read_from_path(&config_file_path).await?; + let maybe_raw_config = RawConfig::read_from_path(&config_file_path).await?; notifications_sender .send(BackendNotification::Loading { step: LoadingStep::ConfigurationReadSuccessfully { - configuration_exists: maybe_config.is_some(), + configuration_exists: maybe_raw_config.is_some(), }, progress: 0.0, }) .await?; - Ok((config_file_path, maybe_config)) + Ok((config_file_path, maybe_raw_config)) } /// Returns `Ok(None)` if configuration failed validation async fn check_configuration( - config: &RawConfig, + raw_config: &RawConfig, notifications_sender: &mut mpsc::Sender, ) -> anyhow::Result> { notifications_sender @@ -644,7 +628,13 @@ async fn check_configuration( }) .await?; - match Config::try_from_raw_config(config).await { + notifications_sender + .send(BackendNotification::ConfigurationFound { + raw_config: raw_config.clone(), + }) + .await?; + + match Config::try_from_raw_config(raw_config).await { Ok(config) => { notifications_sender .send(BackendNotification::Loading { @@ -656,10 +646,7 @@ async fn check_configuration( } Err(error) => { notifications_sender - .send(BackendNotification::ConfigurationIsInvalid { - config: config.clone(), - error, - }) + .send(BackendNotification::ConfigurationIsInvalid { error }) .await?; Ok(None) diff --git a/src/backend/config.rs b/src/backend/config.rs index 17c64489..851452ff 100644 --- a/src/backend/config.rs +++ b/src/backend/config.rs @@ -6,9 +6,8 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use subspace_core_primitives::PublicKey; use subspace_farmer::utils::ss58::{parse_ss58_reward_address, Ss58ParsingError}; -use tokio::fs; -use tokio::fs::OpenOptions; use tokio::io::AsyncWriteExt; +use tokio::task; const DEFAULT_SUBSTRATE_PORT: u16 = 30333; const DEFAULT_SUBSPACE_PORT: u16 = 30433; @@ -90,7 +89,7 @@ impl RawConfig { }; let app_config_dir = config_local_dir.join(env!("CARGO_PKG_NAME")); - let config_file_path = match fs::create_dir(&app_config_dir).await { + let config_file_path = match tokio::fs::create_dir(&app_config_dir).await { Ok(()) => app_config_dir.join("config.json"), Err(error) => { if error.kind() == io::ErrorKind::AlreadyExists { @@ -105,7 +104,7 @@ impl RawConfig { } pub async fn read_from_path(config_file_path: &Path) -> Result, RawConfigError> { - match fs::read_to_string(config_file_path).await { + match tokio::fs::read_to_string(config_file_path).await { Ok(config) => serde_json::from_str::(&config) .map(Some) .map_err(RawConfigError::FailedToDeserialize), @@ -120,7 +119,7 @@ impl RawConfig { } pub async fn write_to_path(&self, config_file_path: &Path) -> io::Result<()> { - let mut options = OpenOptions::new(); + let mut options = tokio::fs::OpenOptions::new(); options.write(true).truncate(true).create(true); #[cfg(unix)] options.mode(0o600); @@ -198,14 +197,14 @@ impl Config { })?; let node_path = raw_config.node_path().clone(); - check_path(&node_path).await?; + check_path(node_path.clone()).await?; let mut farms = Vec::with_capacity(raw_config.farms().len()); for farm in raw_config.farms() { let path = PathBuf::from(&farm.path); - check_path(&path).await?; + check_path(path.clone()).await?; let size = ByteSize::from_str(&farm.size) .map_err(|error| ConfigError::InvalidSizeFormat { @@ -229,35 +228,62 @@ impl Config { } } -async fn check_path(path: &Path) -> Result<(), ConfigError> { - let exists = fs::try_exists(&path) - .await - .map_err(|error| ConfigError::PathError { +async fn check_path(path: PathBuf) -> Result<(), ConfigError> { + let path_string = path.display().to_string(); + task::spawn_blocking(move || { + let exists = path.try_exists().map_err(|error| ConfigError::PathError { path: path.display().to_string(), error, })?; - if !exists { - let Some(parent) = path.parent() else { - return Err(ConfigError::InvalidPath { + if exists { + // Try to create a temporary file to check if path is writable + tempfile::tempfile_in(&path).map_err(|error| ConfigError::PathError { path: path.display().to_string(), - }); - }; + error: io::Error::new( + io::ErrorKind::PermissionDenied, + format!("Path not writable: {error}"), + ), + })?; + } else { + let Some(parent) = path.parent() else { + return Err(ConfigError::InvalidPath { + path: path.display().to_string(), + }); + }; - let parent_exists = - fs::try_exists(parent) - .await + let parent_exists = parent + .try_exists() .map_err(|error| ConfigError::PathError { path: path.display().to_string(), error, })?; - if !parent_exists { - return Err(ConfigError::InvalidPath { + if !parent_exists { + return Err(ConfigError::InvalidPath { + path: path.display().to_string(), + }); + } + + // Try to create a temporary file in parent directory to check if path is writable, and + // it would be possible to create a parent directory later + tempfile::tempfile_in(parent).map_err(|error| ConfigError::PathError { path: path.display().to_string(), - }); + error: io::Error::new( + io::ErrorKind::PermissionDenied, + format!("Path doesn't exist and can't be created: {error}"), + ), + })?; } - } - Ok(()) + Ok(()) + }) + .await + .map_err(|error| ConfigError::PathError { + path: path_string, + error: io::Error::new( + io::ErrorKind::Other, + format!("Failed to spawn tokio task: {error}"), + ), + })? } diff --git a/src/frontend/configuration.rs b/src/frontend/configuration.rs index b50123c2..3cfe6e3b 100644 --- a/src/frontend/configuration.rs +++ b/src/frontend/configuration.rs @@ -1,11 +1,13 @@ mod farm; +mod utils; use crate::backend::config::{NetworkConfiguration, RawConfig}; use crate::frontend::configuration::farm::{ FarmWidget, FarmWidgetInit, FarmWidgetInput, FarmWidgetOutput, }; +use crate::frontend::configuration::utils::is_directory_writable; use gtk::prelude::*; -use relm4::factory::FactoryVecDeque; +use relm4::factory::AsyncFactoryVecDeque; use relm4::prelude::*; use relm4_components::open_dialog::{ OpenDialog, OpenDialogMsg, OpenDialogResponse, OpenDialogSettings, @@ -32,7 +34,10 @@ pub enum ConfigurationInput { SubspacePortChanged(u16), FasterNetworkingChanged(bool), Delete(DynamicIndex), - Reconfigure(RawConfig), + Reinitialize { + raw_config: RawConfig, + reconfiguration: bool, + }, Help, Start, Back, @@ -135,7 +140,7 @@ pub struct ConfigurationView { #[do_not_track] node_path: MaybeValid, #[no_eq] - farms: FactoryVecDeque, + farms: AsyncFactoryVecDeque, #[do_not_track] network_configuration: NetworkConfigurationWrapper, #[do_not_track] @@ -146,8 +151,8 @@ pub struct ConfigurationView { reconfiguration: bool, } -#[relm4::component(pub)] -impl Component for ConfigurationView { +#[relm4::component(pub async)] +impl AsyncComponent for ConfigurationView { type Init = gtk::Window; type Input = ConfigurationInput; type Output = ConfigurationOutput; @@ -224,6 +229,14 @@ impl Component for ConfigurationView { set_label: "Select", }, }, + + gtk::Label { + add_css_class: "error-label", + set_halign: gtk::Align::Start, + set_label: "Folder doesn't exist or user is lacking write permissions", + #[track = "self.node_path.changed_is_valid()"] + set_visible: !model.node_path.is_valid && model.node_path.value != PathBuf::new(), + }, }, }, gtk::ListBoxRow { @@ -435,10 +448,11 @@ impl Component for ConfigurationView { add_css_class: "suggested-action", connect_clicked => ConfigurationInput::Save, #[track = "model.reward_address.changed_is_valid() || model.node_path.changed_is_valid() || model.changed_farms()"] - set_sensitive: model.reward_address.is_valid - && model.node_path.is_valid - && !model.farms.is_empty() - && model.farms.iter().all(FarmWidget::valid), + set_sensitive: + model.reward_address.is_valid + && model.node_path.is_valid + && !model.farms.is_empty() + && model.farms.iter().all(|maybe_farm| maybe_farm.map(FarmWidget::valid).unwrap_or_default()), gtk::Label { set_label: "Save", @@ -477,7 +491,7 @@ impl Component for ConfigurationView { model.reward_address.is_valid && model.node_path.is_valid && !model.farms.is_empty() - && model.farms.iter().all(FarmWidget::valid), + && model.farms.iter().all(|maybe_farm| maybe_farm.map(FarmWidget::valid).unwrap_or_default()), gtk::Label { set_label: "Start", @@ -492,11 +506,11 @@ impl Component for ConfigurationView { } } - fn init( + async fn init( parent_root: Self::Init, root: Self::Root, - sender: ComponentSender, - ) -> ComponentParts { + sender: AsyncComponentSender, + ) -> AsyncComponentParts { let open_dialog = OpenDialog::builder() .transient_for_native(&parent_root) .launch(OpenDialogSettings { @@ -509,7 +523,7 @@ impl Component for ConfigurationView { OpenDialogResponse::Cancel => ConfigurationInput::Ignore, }); - let mut farms = FactoryVecDeque::builder() + let mut farms = AsyncFactoryVecDeque::builder() .launch(gtk::ListBox::new()) .forward(sender.input_sender(), |output| match output { FarmWidgetOutput::OpenDirectory(index) => { @@ -535,22 +549,31 @@ impl Component for ConfigurationView { let configuration_list_box = model.farms.widget(); let widgets = view_output!(); - ComponentParts { model, widgets } + AsyncComponentParts { model, widgets } } - fn update(&mut self, input: Self::Input, sender: ComponentSender, _root: &Self::Root) { + async fn update( + &mut self, + input: Self::Input, + sender: AsyncComponentSender, + _root: &Self::Root, + ) { // Reset changes self.reset(); self.reward_address.reset(); self.node_path.reset(); self.network_configuration.reset(); - self.process_input(input, sender); + self.process_input(input, sender).await; } } impl ConfigurationView { - fn process_input(&mut self, input: ConfigurationInput, sender: ComponentSender) { + async fn process_input( + &mut self, + input: ConfigurationInput, + sender: AsyncComponentSender, + ) { match input { ConfigurationInput::AddFarm => { self.get_mut_farms() @@ -564,7 +587,11 @@ impl ConfigurationView { ConfigurationInput::DirectorySelected(path) => { match self.pending_directory_selection.take() { Some(DirectoryKind::NodePath) => { - self.node_path = MaybeValid::yes(path); + self.node_path = if is_directory_writable(path.clone()).await { + MaybeValid::yes(path) + } else { + MaybeValid::no(path) + }; } Some(DirectoryKind::FarmPath(index)) => { self.get_mut_farms().send( @@ -603,22 +630,34 @@ impl ConfigurationView { .set_is_valid(parse_ss58_reward_address(new_reward_address).is_ok()); self.reward_address.value = new_reward_address.to_string(); } - ConfigurationInput::Reconfigure(raw_config) => { - self.reward_address = MaybeValid::yes(raw_config.reward_address().to_string()); - self.node_path = MaybeValid::yes(raw_config.node_path().clone()); + ConfigurationInput::Reinitialize { + raw_config, + reconfiguration, + } => { + let new_reward_address = raw_config.reward_address().trim(); + self.reward_address + .set_is_valid(parse_ss58_reward_address(new_reward_address).is_ok()); + self.reward_address + .set_value(new_reward_address.to_string()); + + self.node_path = if is_directory_writable(raw_config.node_path().clone()).await { + MaybeValid::yes(raw_config.node_path().clone()) + } else { + MaybeValid::no(raw_config.node_path().clone()) + }; { let mut farms = self.get_mut_farms().guard(); farms.clear(); for farm in raw_config.farms() { farms.push_back(FarmWidgetInit { - path: MaybeValid::yes(farm.path.clone()), - size: MaybeValid::yes(farm.size.clone()), + path: farm.path.clone(), + size: farm.size.clone(), }); } } self.network_configuration = NetworkConfigurationWrapper::from(raw_config.network()); - self.reconfiguration = true; + self.reconfiguration = reconfiguration; } ConfigurationInput::Help => { if let Err(error) = open::that_detached( @@ -628,11 +667,10 @@ impl ConfigurationView { } } ConfigurationInput::Start => { - if sender - .output(ConfigurationOutput::StartWithNewConfig( - self.create_raw_config(), - )) - .is_err() + if let Some(raw_config) = self.create_raw_config() + && sender + .output(ConfigurationOutput::StartWithNewConfig(raw_config)) + .is_err() { debug!("Failed to send ConfigurationOutput::StartWithNewConfig"); } @@ -648,9 +686,10 @@ impl ConfigurationView { } } ConfigurationInput::Save => { - if sender - .output(ConfigurationOutput::ConfigUpdate(self.create_raw_config())) - .is_err() + if let Some(raw_config) = self.create_raw_config() + && sender + .output(ConfigurationOutput::ConfigUpdate(raw_config)) + .is_err() { debug!("Failed to send ConfigurationOutput::ConfigUpdate"); } @@ -666,16 +705,20 @@ impl ConfigurationView { } /// Create raw config from own state - fn create_raw_config(&self) -> RawConfig { - RawConfig::V0 { + fn create_raw_config(&self) -> Option { + Some(RawConfig::V0 { reward_address: String::clone(&self.reward_address), node_path: PathBuf::clone(&self.node_path), - farms: self.farms.iter().map(FarmWidget::farm).collect(), + farms: self + .farms + .iter() + .map(|maybe_farm_widget| Some(maybe_farm_widget?.farm())) + .collect::>>()?, network: NetworkConfiguration { substrate_port: self.network_configuration.substrate_port, subspace_port: self.network_configuration.subspace_port, faster_networking: self.network_configuration.faster_networking, }, - } + }) } } diff --git a/src/frontend/configuration/farm.rs b/src/frontend/configuration/farm.rs index f42fe648..514c4c32 100644 --- a/src/frontend/configuration/farm.rs +++ b/src/frontend/configuration/farm.rs @@ -2,7 +2,12 @@ use crate::backend::config::Farm; use crate::frontend::configuration::MaybeValid; use bytesize::ByteSize; use gtk::prelude::*; +// TODO: Remove import once in prelude: https://github.com/Relm4/Relm4/issues/662 +use relm4::factory::AsyncFactoryComponent; use relm4::prelude::*; +// TODO: Remove import once in prelude: https://github.com/Relm4/Relm4/issues/662 +use crate::frontend::configuration::utils::is_directory_writable; +use relm4::AsyncFactorySender; use relm4_icons::icon_name; use std::path::PathBuf; use std::str::FromStr; @@ -11,17 +16,23 @@ use tracing::warn; // 2 GB const MIN_FARM_SIZE: u64 = 1000 * 1000 * 1000 * 2; +fn is_size_valid(size: &str) -> bool { + ByteSize::from_str(size) + .map(|size| size.as_u64() >= MIN_FARM_SIZE) + .unwrap_or_default() +} + #[derive(Debug)] pub(super) struct FarmWidgetInit { - pub(super) path: MaybeValid, - pub(super) size: MaybeValid, + pub(super) path: PathBuf, + pub(super) size: String, } impl Default for FarmWidgetInit { fn default() -> Self { Self { - path: MaybeValid::no(PathBuf::new()), - size: MaybeValid::no(String::new()), + path: PathBuf::new(), + size: String::new(), } } } @@ -45,11 +56,10 @@ pub(super) struct FarmWidget { index: DynamicIndex, path: MaybeValid, size: MaybeValid, - valid: bool, } -#[relm4::factory(pub(super))] -impl FactoryComponent for FarmWidget { +#[relm4::factory(pub(super) async)] +impl AsyncFactoryComponent for FarmWidget { type Init = FarmWidgetInit; type Input = FarmWidgetInput; type Output = FarmWidgetOutput; @@ -163,42 +173,61 @@ impl FactoryComponent for FarmWidget { set_tooltip: "Delete this farm", }, }, + + gtk::Label { + add_css_class: "error-label", + set_halign: gtk::Align::Start, + set_label: "Folder doesn't exist or user is lacking write permissions", + #[track = "self.path.changed_is_valid()"] + set_visible: !self.path.is_valid && self.path.value != PathBuf::new(), + }, }, } } - fn init_model(value: Self::Init, index: &DynamicIndex, _sender: FactorySender) -> Self { + async fn init_model( + value: Self::Init, + index: &DynamicIndex, + _sender: AsyncFactorySender, + ) -> Self { Self { index: index.clone(), - path: value.path, - size: value.size, - valid: false, + path: if is_directory_writable(value.path.clone()).await { + MaybeValid::yes(value.path) + } else { + MaybeValid::no(value.path) + }, + size: if is_size_valid(&value.size) { + MaybeValid::yes(value.size) + } else { + MaybeValid::no(value.size) + }, } } - fn update(&mut self, input: Self::Input, sender: FactorySender) { + async fn update(&mut self, input: Self::Input, sender: AsyncFactorySender) { // Reset changes self.path.reset(); self.size.reset(); + let was_valid = self.valid(); + match input { FarmWidgetInput::DirectorySelected(path) => { - self.path = MaybeValid::yes(path); + self.path = if is_directory_writable(path.clone()).await { + MaybeValid::yes(path) + } else { + MaybeValid::no(path) + }; } FarmWidgetInput::FarmSizeChanged(size) => { - self.size.set_is_valid( - ByteSize::from_str(&size) - .map(|size| size.as_u64() >= MIN_FARM_SIZE) - .unwrap_or_default(), - ); + self.size.set_is_valid(is_size_valid(&size)); self.size.value = size; } } - let valid = self.valid(); - if self.valid != valid { - self.valid = valid; - + let is_valid = self.valid(); + if was_valid != is_valid { // Send notification up that validity was updated, such that parent view can re-render // view if necessary if sender.output(FarmWidgetOutput::ValidityUpdate).is_err() { diff --git a/src/frontend/configuration/utils.rs b/src/frontend/configuration/utils.rs new file mode 100644 index 00000000..d930177c --- /dev/null +++ b/src/frontend/configuration/utils.rs @@ -0,0 +1,21 @@ +use std::path::PathBuf; +use tokio::task; + +pub(super) async fn is_directory_writable(path: PathBuf) -> bool { + task::spawn_blocking(move || { + if path.exists() { + // Try to create a temporary file to check if path is writable + tempfile::tempfile_in(path).is_ok() + } else { + // Try to create a temporary file in parent directory to check if path is writable, and + // it would be possible to create a parent directory later + if let Some(parent) = path.parent() { + tempfile::tempfile_in(parent).is_ok() + } else { + false + } + } + }) + .await + .unwrap_or_default() +} diff --git a/src/main.rs b/src/main.rs index 685708a6..3a1fb4e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -123,6 +123,7 @@ enum AppInput { InitialConfiguration, StartUpgrade, Restart, + CloseStatusBarWarning, ShowWindow, Quit, } @@ -165,6 +166,8 @@ enum StatusBarNotification { None, Warning { message: String, + /// Whether to show ok button + ok: bool, /// Whether to show restart button restart: bool, }, @@ -191,6 +194,13 @@ impl StatusBarNotification { } } + fn ok_button(&self) -> bool { + match self { + Self::Warning { ok, .. } => *ok, + _ => false, + } + } + fn restart_button(&self) -> bool { match self { Self::Warning { restart, .. } => *restart, @@ -218,7 +228,7 @@ struct App { #[do_not_track] loading_view: Controller, #[do_not_track] - configuration_view: Controller, + configuration_view: AsyncController, #[do_not_track] running_view: Controller, #[do_not_track] @@ -431,6 +441,13 @@ impl AsyncComponent for App { #[track = "model.changed_status_bar_notification()"] set_visible: model.status_bar_notification.restart_button(), }, + + gtk::Button { + connect_clicked => AppInput::CloseStatusBarWarning, + set_label: "Ok", + #[track = "model.changed_status_bar_notification()"] + set_visible: model.status_bar_notification.ok_button(), + }, }, }, } @@ -640,9 +657,18 @@ impl AsyncComponent for App { } AppInput::OpenReconfiguration => { self.menu_popover.hide(); - if let Some(raw_config) = self.current_raw_config.clone() { + let configuration_already_opened = matches!( + self.current_view, + View::Configuration | View::Reconfiguration + ); + if !configuration_already_opened + && let Some(raw_config) = self.current_raw_config.clone() + { self.configuration_view - .emit(ConfigurationInput::Reconfigure(raw_config)); + .emit(ConfigurationInput::Reinitialize { + raw_config, + reconfiguration: true, + }); self.set_current_view(View::Reconfiguration); } } @@ -673,6 +699,9 @@ impl AsyncComponent for App { *self.exit_status_code.lock() = AppStatusCode::Restart; relm4::main_application().quit(); } + AppInput::CloseStatusBarWarning => { + self.set_status_bar_notification(StatusBarNotification::None); + } AppInput::ShowWindow => { root.present(); } @@ -704,7 +733,10 @@ impl App { error!(%error, path = %app_data_dir.display(), "Failed to open logs folder"); } } + fn process_backend_notification(&mut self, notification: BackendNotification) { + debug!(?notification, "New backend notification"); + match notification { // TODO: Render progress BackendNotification::Loading { step, progress: _ } => { @@ -712,22 +744,35 @@ impl App { self.set_status_bar_notification(StatusBarNotification::None); self.loading_view.emit(LoadingInput::BackendLoading(step)); } - BackendNotification::IncompatibleChain { - raw_config, - compatible_chain, - } => { - self.get_mut_current_raw_config().replace(raw_config); + BackendNotification::ConfigurationFound { raw_config } => { + self.get_mut_current_raw_config() + .replace(raw_config.clone()); + } + BackendNotification::IncompatibleChain { compatible_chain } => { self.set_current_view(View::Upgrade { chain_name: compatible_chain, }); } BackendNotification::NotConfigured => { - self.set_current_view(View::Welcome); + if self.current_raw_config.is_none() { + self.set_current_view(View::Welcome); + } else { + self.set_current_view(View::Configuration); + } } - BackendNotification::ConfigurationIsInvalid { error, .. } => { - self.set_status_bar_notification(StatusBarNotification::Error(format!( - "Configuration is invalid: {error}" - ))); + BackendNotification::ConfigurationIsInvalid { error } => { + if let Some(raw_config) = self.current_raw_config.clone() { + self.configuration_view + .emit(ConfigurationInput::Reinitialize { + raw_config, + reconfiguration: false, + }); + } + self.set_status_bar_notification(StatusBarNotification::Warning { + message: format!("Configuration is invalid: {error}",), + ok: true, + restart: false, + }); } BackendNotification::ConfigSaveResult(result) => match result { Ok(()) => { @@ -735,6 +780,7 @@ impl App { message: "Application restart is needed for configuration changes to take effect" .to_string(), + ok: false, restart: true, }); }