From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 963E61FF17A for ; Tue, 9 Dec 2025 15:35:44 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id C0C30442A; Tue, 9 Dec 2025 15:36:23 +0100 (CET) From: Dietmar Maurer To: yew-devel@lists.proxmox.com Date: Tue, 9 Dec 2025 14:11:58 +0100 Message-ID: <20251209131159.4027954-1-dietmar@proxmox.com> X-Mailer: git-send-email 2.47.3 MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.576 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record X-Mailman-Approved-At: Tue, 09 Dec 2025 15:36:23 +0100 Subject: [yew-devel] [RFC yew-comp] refactor: move LoadableComponent state into component implementations X-BeenThere: yew-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Yew framework devel list at Proxmox List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Yew framework devel list at Proxmox Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: yew-devel-bounces@lists.proxmox.com Sender: "yew-devel" Major Refactoring of the `LoadableComponent` system. Encapsulate component state (loading, error, view_state) into a new `LoadableComponentState` struct. Instead of `LoadableComponentMaster` managing the state externally, the concrete components now own their `LoadableComponentState`. - Use `impl_deref_mut_property`LoadableComponentState` struct. to implement Deref/DerefMut for components, allowing `LoadableComponentMaster` to access the state transparently. - `LoadableComponentContext` is now a normal yew Scope (removed custom implementation) - Migrate all `LoadableComponent` implementations (ACL, ACME, APT, Network, User/Token/TFA views, etc.) to the new pattern. - Use `link.custom_callback` and `link.send_custom_message` for internal messaging (rename is necessaray because of naming conflict with standard Scope function). - avoid useless Redraw/Datachange/Refresh messages, because `LoadableComponentMaster` already implements that. Signed-off-by: Dietmar Maurer --- src/acl/acl_view.rs | 26 +- src/acme/acme_accounts.rs | 28 +- src/acme/acme_domains.rs | 35 +- src/acme/acme_plugins.rs | 60 +- src/acme/certificate_list.rs | 36 +- src/apt_package_manager.rs | 45 +- src/apt_repositories.rs | 44 +- src/auth_view.rs | 36 +- src/configuration/network_view.rs | 40 +- src/configuration/pve/lxc_network_panel.rs | 31 +- src/lib.rs | 33 +- src/loadable_component.rs | 698 ++++++++++++--------- src/node_status_panel.rs | 37 +- src/notes_view.rs | 16 +- src/object_grid.rs | 36 +- src/permission_panel.rs | 17 +- src/subscription_panel.rs | 33 +- src/tasks.rs | 39 +- src/tfa/tfa_view.rs | 36 +- src/token_panel.rs | 24 +- src/user_panel.rs | 31 +- 21 files changed, 825 insertions(+), 556 deletions(-) diff --git a/src/acl/acl_view.rs b/src/acl/acl_view.rs index 58da3fd..9347d72 100644 --- a/src/acl/acl_view.rs +++ b/src/acl/acl_view.rs @@ -24,7 +24,9 @@ use proxmox_access_control::types::{AclListItem, AclUgidType}; use crate::percent_encoding::percent_encode_component; use crate::utils::render_boolean; use crate::{ - ConfirmButton, EditWindow, LoadableComponent, LoadableComponentContext, LoadableComponentMaster, + impl_deref_mut_property, ConfirmButton, EditWindow, LoadableComponent, + LoadableComponentContext, LoadableComponentMaster, LoadableComponentScopeExt, + LoadableComponentState, }; use super::acl_edit::AclEditWindow; @@ -95,15 +97,17 @@ enum ViewState { } enum Msg { - Reload, Remove, } struct ProxmoxAclView { + state: LoadableComponentState, selection: Selection, store: Store, } +impl_deref_mut_property!(ProxmoxAclView, state, LoadableComponentState); + impl ProxmoxAclView { fn colmuns() -> Rc>> { Rc::new(vec![ @@ -141,14 +145,21 @@ impl LoadableComponent for ProxmoxAclView { let link = ctx.link(); link.repeated_load(5000); - let selection = Selection::new().on_select(link.callback(|_| Msg::Reload)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); let store = Store::with_extract_key(|record: &AclListItem| { let acl_id = format!("{} for {} - {}", record.path, record.ugid, record.roleid); Key::from(acl_id) }); - Self { selection, store } + Self { + state: LoadableComponentState::new(), + selection, + store, + } } fn load( @@ -203,7 +214,7 @@ impl LoadableComponent for ProxmoxAclView { ConfirmButton::new(tr!("Remove ACL Entry")) .confirm_message(tr!("Are you sure you want to remove this ACL entry?")) .disabled(disabled) - .on_activate(ctx.link().callback(|_| Msg::Remove)), + .on_activate(ctx.link().custom_callback(|_| Msg::Remove)), ); Some(toolbar.into()) @@ -211,14 +222,13 @@ impl LoadableComponent for ProxmoxAclView { fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { match msg { - Msg::Reload => true, Msg::Remove => { if let Some(key) = self.selection.selected_key() { if let Some(record) = self.store.read().lookup_record(&key).cloned() { - let link = ctx.link(); + let link = ctx.link().clone(); let url = ctx.props().acl_api_endpoint.to_owned(); - link.clone().spawn(async move { + self.spawn(async move { let data = match record.ugid_type { AclUgidType::User => json!({ "delete": true, diff --git a/src/acme/acme_accounts.rs b/src/acme/acme_accounts.rs index 19bf48b..4c4db1a 100644 --- a/src/acme/acme_accounts.rs +++ b/src/acme/acme_accounts.rs @@ -15,8 +15,9 @@ use crate::common_api_types::AcmeAccountInfo; use crate::percent_encoding::percent_encode_component; use crate::utils::render_url; use crate::{ - ConfirmButton, DataViewWindow, LoadableComponent, LoadableComponentContext, - LoadableComponentMaster, + impl_deref_mut_property, ConfirmButton, DataViewWindow, LoadableComponent, + LoadableComponentContext, LoadableComponentMaster, LoadableComponentScopeExt, + LoadableComponentState, }; use super::AcmeRegisterAccount; @@ -44,14 +45,17 @@ impl AcmeAccountsPanel { #[doc(hidden)] pub struct ProxmoxAcmeAccountsPanel { + state: LoadableComponentState, selection: Selection, store: Store, columns: Rc>>, } -pub enum Msg { - Redraw, -} +impl_deref_mut_property!( + ProxmoxAcmeAccountsPanel, + state, + LoadableComponentState +); #[derive(PartialEq)] pub enum ViewState { @@ -61,11 +65,14 @@ pub enum ViewState { impl LoadableComponent for ProxmoxAcmeAccountsPanel { type Properties = AcmeAccountsPanel; - type Message = Msg; + type Message = (); type ViewState = ViewState; fn create(ctx: &LoadableComponentContext) -> Self { - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); let store = Store::with_extract_key(|record: &AcmeAccountEntry| Key::from(record.name.clone())); @@ -77,6 +84,7 @@ impl LoadableComponent for ProxmoxAcmeAccountsPanel { .into()]); Self { + state: LoadableComponentState::new(), selection, store, columns, @@ -110,7 +118,7 @@ impl LoadableComponent for ProxmoxAcmeAccountsPanel { Button::new(tr!("View")) .disabled(selected_key.is_none()) .onclick({ - let link = ctx.link(); + let link = ctx.link().clone(); let selected_key = selected_key.clone(); move |_| { if let Some(selected_key) = &selected_key { @@ -123,7 +131,7 @@ impl LoadableComponent for ProxmoxAcmeAccountsPanel { ConfirmButton::remove_entry(selected_key.as_deref().unwrap_or("").to_string()) .disabled(selected_key.is_none()) .on_activate({ - let link = ctx.link(); + let link = ctx.link().clone(); let selected_key = selected_key.clone(); move |_| { @@ -159,7 +167,7 @@ impl LoadableComponent for ProxmoxAcmeAccountsPanel { .selection(self.selection.clone()) .on_row_dblclick({ let selection = self.selection.clone(); - let link = ctx.link(); + let link = ctx.link().clone(); move |_: &mut _| { if let Some(selected_key) = selection.selected_key() { link.change_view(Some(ViewState::View(selected_key.clone()))); diff --git a/src/acme/acme_domains.rs b/src/acme/acme_domains.rs index 6ab924d..32003d8 100644 --- a/src/acme/acme_domains.rs +++ b/src/acme/acme_domains.rs @@ -19,8 +19,10 @@ use pwt_macros::builder; use crate::common_api_types::{create_acme_config_string, parse_acme_config_string, AcmeConfig}; use crate::common_api_types::{create_acme_domain_string, parse_acme_domain_string, AcmeDomain}; use crate::percent_encoding::percent_encode_component; -use crate::{ConfirmButton, EditWindow}; -use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster}; +use crate::{impl_deref_mut_property, ConfirmButton, EditWindow, LoadableComponentState}; +use crate::{ + LoadableComponent, LoadableComponentContext, LoadableComponentMaster, LoadableComponentScopeExt, +}; use super::{AcmeAccountSelector, AcmeChallengeTypeSelector, AcmePluginSelector}; @@ -54,14 +56,20 @@ impl AcmeDomainsPanel { #[doc(hidden)] pub struct ProxmoxAcmeDomainsPanel { + state: LoadableComponentState, selection: Selection, store: Store, columns: Rc>>, acme_account: Option, } +impl_deref_mut_property!( + ProxmoxAcmeDomainsPanel, + state, + LoadableComponentState +); + pub enum Msg { - Redraw, AcmeAccount(Option), } @@ -78,7 +86,10 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel { type ViewState = ViewState; fn create(ctx: &LoadableComponentContext) -> Self { - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); let store = Store::with_extract_key(|record: &AcmeDomainEntry| { Key::from(record.config_key.clone()) }); @@ -109,6 +120,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel { ctx.link().repeated_load(3000); Self { + state: LoadableComponentState::new(), selection, store, columns, @@ -122,7 +134,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel { ) -> Pin>>> { let store = self.store.clone(); let url = ctx.props().url.clone(); - let link = ctx.link(); + let link = ctx.link().clone(); Box::pin(async move { let data: Value = crate::http_get(&*url, None).await?; @@ -147,9 +159,9 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel { if let Some(Value::String(acme_account)) = data.get("acme") { let acme_account = parse_acme_config_string(acme_account)?; - link.send_message(Msg::AcmeAccount(Some(acme_account))); + link.send_custom_message(Msg::AcmeAccount(Some(acme_account))); } else { - link.send_message(Msg::AcmeAccount(None)); + link.send_custom_message(Msg::AcmeAccount(None)); } Ok(()) }) @@ -157,7 +169,6 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel { fn update(&mut self, _ctx: &LoadableComponentContext, msg: Self::Message) -> bool { match msg { - Msg::Redraw => true, Msg::AcmeAccount(acme_account) => { self.acme_account = acme_account; true @@ -179,7 +190,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel { Button::new(tr!("Edit")) .disabled(selected_key.is_none()) .onclick({ - let link = ctx.link(); + let link = ctx.link().clone(); let selected_key = selected_key.clone(); move |_| { if let Some(selected_key) = &selected_key { @@ -192,7 +203,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel { ConfirmButton::remove_entry(selected_key.as_deref().unwrap_or("").to_string()) .disabled(selected_key.is_none()) .on_activate({ - let link = ctx.link(); + let link = ctx.link().clone(); let url = ctx.props().url.clone(); let selected_key = selected_key.clone(); move |_| { @@ -238,7 +249,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel { } }) .with_child(Button::new(tr!("Order Certificate Now")).onclick({ - let link = ctx.link(); + let link = ctx.link().clone(); move |_| { let command_path = "/nodes/localhost/certificates/acme/certificate"; link.start_task(command_path, None, false); @@ -253,7 +264,7 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel { .class("pwt-flex-fit") .selection(self.selection.clone()) .on_row_dblclick({ - let link = ctx.link(); + let link = ctx.link().clone(); move |event: &mut DataTableMouseEvent| { let key = &event.record_key; link.change_view(Some(ViewState::Edit(key.clone()))); diff --git a/src/acme/acme_plugins.rs b/src/acme/acme_plugins.rs index f0b1e69..66baa5f 100644 --- a/src/acme/acme_plugins.rs +++ b/src/acme/acme_plugins.rs @@ -9,19 +9,20 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use yew::virtual_dom::{Key, VComp, VNode}; +use pwt::prelude::*; use pwt::state::{Selection, Store}; use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader, DataTableMouseEvent}; use pwt::widget::form::{DisplayField, Field, FormContext, Number, TextArea}; use pwt::widget::{Button, InputPanel, Toolbar}; -use pwt::{prelude::*, AsyncPool}; use pwt_macros::builder; use crate::form::delete_empty_values; use crate::percent_encoding::percent_encode_component; use crate::{ - http_get, ConfirmButton, EditWindow, LoadableComponent, LoadableComponentContext, - LoadableComponentLink, LoadableComponentMaster, + http_get, impl_deref_mut_property, ConfirmButton, EditWindow, LoadableComponent, + LoadableComponentContext, LoadableComponentMaster, LoadableComponentScope, + LoadableComponentScopeExt, LoadableComponentState, }; use super::{AcmeChallengeSchemaItem, AcmeChallengeSelector}; @@ -81,14 +82,20 @@ struct ChallengeSchemaInfo { #[doc(hidden)] pub struct ProxmoxAcmePluginsPanel { + state: LoadableComponentState, selection: Selection, store: Store, columns: Rc>>, challenge_schema: Option, schema_info: ChallengeSchemaInfo, - async_pool: AsyncPool, } +impl_deref_mut_property!( + ProxmoxAcmePluginsPanel, + state, + LoadableComponentState +); + #[derive(PartialEq)] pub enum ViewState { Add, @@ -96,7 +103,6 @@ pub enum ViewState { } pub enum Msg { - Redraw, CloseDialog, Add, Edit(Key), @@ -126,16 +132,20 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel { type ViewState = ViewState; fn create(ctx: &LoadableComponentContext) -> Self { - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); let store = Store::with_extract_key(|record: &PluginConfig| Key::from(record.plugin.clone())); let schema_name_map = Rc::new(HashMap::new()); let columns = columns(schema_name_map.clone()); - ctx.link().send_message(Msg::LoadChallengeSchemaList); + ctx.link().send_custom_message(Msg::LoadChallengeSchemaList); Self { + state: LoadableComponentState::new(), selection, store, columns, @@ -144,7 +154,6 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel { schema_name_map, store: Store::new(), }, - async_pool: AsyncPool::new(), } } @@ -164,7 +173,6 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel { fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { match msg { - Msg::Redraw => true, Msg::Add => { self.challenge_schema = None; ctx.link().change_view(Some(ViewState::Add)); @@ -217,10 +225,10 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel { } Msg::LoadChallengeSchemaList => { let url = ctx.props().challenge_shema_url.clone(); - let link = ctx.link(); - self.async_pool.spawn(async move { + let link = ctx.link().clone(); + self.spawn(async move { let result = http_get(&*url, None).await; - link.send_message(Msg::UpdateChallengeSchemaList(result)); + link.send_custom_message(Msg::UpdateChallengeSchemaList(result)); }); false } @@ -241,7 +249,7 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel { ); let command_future = crate::http_delete(command_path, None); let link = ctx.link().clone(); - self.async_pool.spawn(async move { + self.spawn(async move { match command_future.await { Ok(()) => { link.send_reload(); @@ -265,16 +273,16 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel { .class("pwt-w-100") .class("pwt-overflow-hidden") .class("pwt-border-bottom") - .with_child(Button::new(tr!("Add")).onclick(ctx.link().callback(|_| Msg::Add))) + .with_child(Button::new(tr!("Add")).onclick(ctx.link().custom_callback(|_| Msg::Add))) .with_child( Button::new(tr!("Edit")) .disabled(selected_key.is_none()) .onclick({ - let link = ctx.link(); + let link = ctx.link().clone(); let selected_key = selected_key.clone(); move |_| { if let Some(selected_key) = &selected_key { - link.send_message(Msg::Edit(selected_key.clone())); + link.send_custom_message(Msg::Edit(selected_key.clone())); } } }), @@ -282,7 +290,7 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel { .with_child( ConfirmButton::remove_entry(selected_key.as_deref().unwrap_or("").to_string()) .disabled(selected_key.is_none()) - .on_activate(ctx.link().callback({ + .on_activate(ctx.link().custom_callback({ let selected_key = selected_key.clone(); move |_| Msg::Delete(selected_key.clone()) })), @@ -297,11 +305,11 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel { .selection(self.selection.clone()) .on_row_dblclick({ let store = self.store.clone(); - let link = ctx.link(); + let link = ctx.link().clone(); move |event: &mut DataTableMouseEvent| { let key = &event.record_key; if store.read().lookup_record(key).is_some() { - link.send_message(Msg::Edit(key.clone())); + link.send_custom_message(Msg::Edit(key.clone())); }; } }) @@ -372,7 +380,7 @@ impl ProxmoxAcmePluginsPanel { } fn dns_plugin_input_panel( - link: &LoadableComponentLink, + link: &LoadableComponentScope, form_ctx: &FormContext, id: Option<&str>, challenge_schema: Option<&AcmeChallengeSchemaItem>, @@ -411,7 +419,7 @@ impl ProxmoxAcmePluginsPanel { AcmeChallengeSelector::with_store(challenge_store) .name("api") .required(true) - .on_change(link.callback(Msg::ChallengeSchema)), + .on_change(link.custom_callback(Msg::ChallengeSchema)), ); if let Some(description) = @@ -463,7 +471,7 @@ impl ProxmoxAcmePluginsPanel { .on_change({ let field_name = field_name.clone(); let form_ctx = form_ctx.clone(); - link.callback(move |v| { + link.custom_callback(move |v| { Msg::ApiDataChange(form_ctx.clone(), field_name.clone(), v) }) }), @@ -495,10 +503,10 @@ impl ProxmoxAcmePluginsPanel { move |url: AttrValue| crate::http_get_full(url.to_string(), None), url.clone(), )) - .on_done(ctx.link().callback(|_| Msg::CloseDialog)) + .on_done(ctx.link().custom_callback(|_| Msg::CloseDialog)) .renderer({ let id = id.to_owned(); - let link = ctx.link(); + let link = ctx.link().clone(); let challenge_schema = self.challenge_schema.clone(); let challenge_store = self.schema_info.store.clone(); move |form_ctx: &FormContext| { @@ -532,9 +540,9 @@ impl ProxmoxAcmePluginsPanel { fn create_add_dns_plugin_dialog(&self, ctx: &crate::LoadableComponentContext) -> Html { EditWindow::new(tr!("Add") + ": " + &tr!("ACME DNS Plugin")) - .on_done(ctx.link().callback(|_| Msg::CloseDialog)) + .on_done(ctx.link().custom_callback(|_| Msg::CloseDialog)) .renderer({ - let link = ctx.link(); + let link = ctx.link().clone(); let challenge_schema = self.challenge_schema.clone(); let challenge_store = self.schema_info.store.clone(); move |form_ctx: &FormContext| { diff --git a/src/acme/certificate_list.rs b/src/acme/certificate_list.rs index d9ce92c..7ac20e6 100644 --- a/src/acme/certificate_list.rs +++ b/src/acme/certificate_list.rs @@ -18,8 +18,9 @@ use pwt::widget::{Button, Container, Dialog, FileButton, MessageBox, Toolbar}; use crate::common_api_types::CertificateInfo; use crate::utils::render_epoch; use crate::{ - ConfirmButton, EditWindow, KVGrid, KVGridRow, LoadableComponent, LoadableComponentContext, - LoadableComponentMaster, + impl_deref_mut_property, ConfirmButton, EditWindow, KVGrid, KVGridRow, LoadableComponent, + LoadableComponentContext, LoadableComponentMaster, LoadableComponentScopeExt, + LoadableComponentState, }; async fn upload_custom_certificate(form_ctx: FormContext) -> Result<(), Error> { @@ -46,10 +47,6 @@ impl CertificateList { } } -pub enum Msg { - Redraw, -} - #[derive(PartialEq)] pub enum ViewState { CertificateView(Rc), @@ -59,24 +56,35 @@ pub enum ViewState { #[doc(hidden)] pub struct ProxmoxCertificateList { + state: LoadableComponentState, selection: Selection, store: Store, columns: Rc>>, rows: Rc>, } +impl_deref_mut_property!( + ProxmoxCertificateList, + state, + LoadableComponentState +); + impl LoadableComponent for ProxmoxCertificateList { type Properties = CertificateList; - type Message = Msg; + type Message = (); type ViewState = ViewState; fn create(ctx: &LoadableComponentContext) -> Self { - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); let store = Store::with_extract_key(|record: &CertificateInfo| Key::from(record.filename.clone())); let columns = Rc::new(columns()); let rows = Rc::new(rows()); Self { + state: LoadableComponentState::new(), selection, store, columns, @@ -121,7 +129,7 @@ impl LoadableComponent for ProxmoxCertificateList { "proxy.pem" )) .on_activate({ - let link = ctx.link(); + let link = ctx.link().clone(); move |_| { let link = link.clone(); let command_path = "/nodes/localhost/certificates/custom".to_string(); @@ -145,7 +153,7 @@ impl LoadableComponent for ProxmoxCertificateList { .disabled(selected_cert.is_none()) .onclick({ let selected_cert = selected_cert.clone(); - let link = ctx.link(); + let link = ctx.link().clone(); move |_| { if let Some(selected_cert) = &selected_cert { let cert_data: Value = @@ -167,7 +175,7 @@ impl LoadableComponent for ProxmoxCertificateList { .selection(self.selection.clone()) .on_row_dblclick({ let store = self.store.clone(); - let link = ctx.link(); + let link = ctx.link().clone(); move |event: &mut DataTableMouseEvent| { let key = &event.record_key; if let Some(selected_cert) = store.read().lookup_record(key).cloned() { @@ -226,10 +234,10 @@ async fn update_field_from_file( impl ProxmoxCertificateList { fn create_upload_custom_certificate(&self, ctx: &LoadableComponentContext) -> Html { - let link = ctx.link(); + let link = ctx.link().clone(); EditWindow::new(tr!("Upload Custom Certificate")) .width(600) - .on_close(ctx.link().change_view_callback(|_| None)) + .on_close(link.change_view_callback(|_| None)) .submit_text(tr!("Upload")) .renderer(move |form_ctx: &FormContext| { Form::new() @@ -287,7 +295,7 @@ impl ProxmoxCertificateList { .into() }) .on_submit({ - let link = ctx.link(); + let link = ctx.link().clone(); move |form_ctx: FormContext| { let link = link.clone(); async move { diff --git a/src/apt_package_manager.rs b/src/apt_package_manager.rs index 82d536f..7264dae 100644 --- a/src/apt_package_manager.rs +++ b/src/apt_package_manager.rs @@ -18,13 +18,14 @@ use pwt::widget::data_table::{ DataTable, DataTableCellRenderArgs, DataTableColumn, DataTableHeader, DataTableHeaderGroup, }; use pwt::widget::{AlertDialog, Button, Container, Toolbar, Tooltip}; -use pwt::AsyncPool; use crate::percent_encoding::percent_encode_component; use crate::subscription_alert::subscription_is_active; +use crate::LoadableComponentState; use crate::SubscriptionAlert; use crate::{ - DataViewWindow, LoadableComponent, LoadableComponentContext, LoadableComponentMaster, XTermJs, + DataViewWindow, LoadableComponent, LoadableComponentContext, LoadableComponentMaster, + LoadableComponentScopeExt, XTermJs, }; use proxmox_apt_api_types::APTUpdateInfo; @@ -163,31 +164,43 @@ pub enum ViewState { /// Messages for [`ProxmoxAptManager::update`] pub enum Msg { CheckSubscription, - SelectionChange, } pub struct ProxmoxAptPackageManager { + state: LoadableComponentState, tree_store: TreeStore, selection: Selection, columns: Rc>>, - async_pool: AsyncPool, } +crate::impl_deref_mut_property!( + ProxmoxAptPackageManager, + state, + LoadableComponentState +); + impl LoadableComponent for ProxmoxAptPackageManager { type Properties = AptPackageManager; type Message = Msg; type ViewState = ViewState; fn create(ctx: &LoadableComponentContext) -> Self { + let props = ctx.props(); let tree_store = TreeStore::new().view_root(false); let columns = Self::columns(ctx, tree_store.clone()); - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::SelectionChange)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); + + let mut state = LoadableComponentState::new(); + state.set_task_base_url(props.task_base_url.clone()); Self { + state, tree_store, selection, columns, - async_pool: AsyncPool::new(), } } @@ -200,14 +213,12 @@ impl LoadableComponent for ProxmoxAptPackageManager { .clone() .subscription_url .unwrap_or("/nodes/localhost/subscription".into()); - let task_base_url = props.task_base_url.clone(); let command = format!("{}/update", props.base_url); - self.async_pool.spawn(async move { + self.spawn(async move { let data = crate::http_get::(url.as_str(), None).await; let is_active = subscription_is_active(Some(&data)); if is_active { - link.task_base_url(task_base_url); link.start_task(&command, None, false); } else { link.change_view(Some(ViewState::ShowSubscriptionPopup)); @@ -215,7 +226,6 @@ impl LoadableComponent for ProxmoxAptPackageManager { }); true } - Msg::SelectionChange => true, } } @@ -264,15 +274,14 @@ impl LoadableComponent for ProxmoxAptPackageManager { .class("pwt-overflow-hidden") .class("pwt-border-bottom") .with_child(Button::new(tr!("Refresh")).on_activate({ - let link = ctx.link(); + let link = ctx.link().clone(); let sub_check = props.subscription_url.is_some(); - link.task_base_url(props.task_base_url.clone()); let command = format!("{}/update", props.base_url); move |_| { if sub_check { - link.send_message(Msg::CheckSubscription); + link.send_custom_message(Msg::CheckSubscription); } else { link.start_task(&command, None, false); } @@ -287,7 +296,7 @@ impl LoadableComponent for ProxmoxAptPackageManager { Button::new(tr!("Changelog")) .disabled(selected_package.is_none()) .onclick({ - let link = ctx.link(); + let link = ctx.link().clone(); let view = selected_package .as_ref() .map(|p| ViewState::ShowChangelog(p.clone())); @@ -296,8 +305,8 @@ impl LoadableComponent for ProxmoxAptPackageManager { ) .with_flex_spacer() .with_child({ - let loading = ctx.loading(); - let link = ctx.link(); + let loading = self.loading(); + let link = ctx.link().clone(); Button::refresh(loading).onclick(move |_| link.send_reload()) }); @@ -325,11 +334,9 @@ impl LoadableComponent for ProxmoxAptPackageManager { ViewState::ShowSubscriptionPopup => { let link = ctx.link().clone(); let props = ctx.props(); - let task_base_url = props.task_base_url.clone(); let command = format!("{}/update", props.base_url); let on_close = move |_| { link.change_view(None); - link.task_base_url(task_base_url.clone()); link.start_task(&command, None, false); }; Some(if let Some(msg) = props.subscription_message.clone() { @@ -351,6 +358,8 @@ impl LoadableComponent for ProxmoxAptPackageManager { ) -> bool { let props = ctx.props(); + self.set_task_base_url(props.task_base_url.clone()); + if props.base_url != old_props.base_url || props.task_base_url != old_props.task_base_url { ctx.link().send_reload(); true diff --git a/src/apt_repositories.rs b/src/apt_repositories.rs index 7157c0c..904c052 100644 --- a/src/apt_repositories.rs +++ b/src/apt_repositories.rs @@ -22,7 +22,8 @@ use pwt::widget::{Button, Column, Container, Fa, Row, Toolbar, Tooltip}; use crate::subscription_alert::subscription_is_active; use crate::{ EditWindow, ExistingProduct, LoadableComponent, LoadableComponentContext, - LoadableComponentMaster, ProjectInfo, SubscriptionAlert, + LoadableComponentMaster, LoadableComponentScopeExt, LoadableComponentState, ProjectInfo, + SubscriptionAlert, }; use pwt_macros::builder; @@ -406,7 +407,6 @@ fn apt_configuration_to_tree(config: &APTRepositoriesResult) -> SlabTree), @@ -419,6 +419,7 @@ pub enum ViewState { } pub struct ProxmoxAptRepositories { + state: LoadableComponentState, tree_store: TreeStore, selection: Selection, columns: Rc>>, @@ -430,6 +431,12 @@ pub struct ProxmoxAptRepositories { status_columns: Rc>>, } +crate::impl_deref_mut_property!( + ProxmoxAptRepositories, + state, + LoadableComponentState +); + impl LoadableComponent for ProxmoxAptRepositories { type Properties = AptRepositories; type Message = Msg; @@ -438,19 +445,27 @@ impl LoadableComponent for ProxmoxAptRepositories { fn create(ctx: &LoadableComponentContext) -> Self { let tree_store = TreeStore::new().view_root(false); let columns = Self::columns(ctx, tree_store.clone()); - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Refresh)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); let status_columns = Self::status_columns(ctx); let subscription_url = ctx.props().subscription_url.clone(); - let link = ctx.link(); - link.send_future(async move { - // TODO: also reload this in load, not only create?! - let data = crate::http_get(subscription_url.to_string(), None).await; - Msg::SubscriptionInfo(data) + let state = LoadableComponentState::new(); + + state.spawn({ + let link = ctx.link().clone(); + async move { + // TODO: also reload this in load, not only create?! + let data = crate::http_get(subscription_url.to_string(), None).await; + link.send_custom_message(Msg::SubscriptionInfo(data)); + } }); Self { + state, tree_store, selection, columns, @@ -470,13 +485,13 @@ impl LoadableComponent for ProxmoxAptRepositories { let props = ctx.props(); let base_url = props.base_url.clone(); let tree_store = self.tree_store.clone(); - let link = ctx.link(); + let link = ctx.link().clone(); Box::pin(async move { let config = apt_configuration(base_url.clone()).await?; let tree = apt_configuration_to_tree(&config); tree_store.write().update_root_tree(tree); - link.send_message(Msg::UpdateStatus(config)); + link.send_custom_message(Msg::UpdateStatus(config)); Ok(()) }) } @@ -484,7 +499,6 @@ impl LoadableComponent for ProxmoxAptRepositories { fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { let props = ctx.props(); match msg { - Msg::Refresh => true, Msg::SubscriptionInfo(status) => { self.subscription_status = Some(status); if let Some(config) = &self.config { @@ -547,7 +561,7 @@ impl LoadableComponent for ProxmoxAptRepositories { }); // fixme: add digest to protect against concurrent changes let url = format!("{}/repositories", props.base_url); - let link = ctx.link(); + let link = ctx.link().clone(); link.clone().spawn(async move { match crate::http_post(url, Some(param)).await { Ok(()) => { @@ -606,12 +620,12 @@ impl LoadableComponent for ProxmoxAptRepositories { tr!("Enable") }) .disabled(enabled.is_none()) - .onclick(ctx.link().callback(|_| Msg::ToggleEnable)) + .onclick(ctx.link().custom_callback(|_| Msg::ToggleEnable)) }) .with_flex_spacer() .with_child({ - let loading = ctx.loading(); - let link = ctx.link(); + let loading = self.loading(); + let link = ctx.link().clone(); Button::refresh(loading).onclick(move |_| link.send_reload()) }); diff --git a/src/auth_view.rs b/src/auth_view.rs index d6e5528..bb3960b 100644 --- a/src/auth_view.rs +++ b/src/auth_view.rs @@ -21,7 +21,8 @@ use pwt_macros::builder; use crate::{ AuthEditLDAP, AuthEditOpenID, EditWindow, LoadableComponent, LoadableComponentContext, - LoadableComponentLink, LoadableComponentMaster, + LoadableComponentMaster, LoadableComponentScope, LoadableComponentScopeExt, + LoadableComponentState, }; use crate::common_api_types::BasicRealmInfo; @@ -76,17 +77,20 @@ pub enum ViewState { } pub enum Msg { - Redraw, Edit, Remove, Sync, } + #[doc(hidden)] pub struct ProxmoxAuthView { + state: LoadableComponentState, selection: Selection, store: Store, } +crate::impl_deref_mut_property!(ProxmoxAuthView, state, LoadableComponentState); + async fn delete_item(base_url: AttrValue, realm: AttrValue) -> Result<(), Error> { let url = format!("{base_url}/{}", percent_encode_component(&realm)); crate::http_delete(&url, None).await?; @@ -95,7 +99,7 @@ async fn delete_item(base_url: AttrValue, realm: AttrValue) -> Result<(), Error> async fn sync_realm( form_ctx: FormContext, - link: LoadableComponentLink, + link: LoadableComponentScope, url: impl Into, ) -> Result<(), Error> { let mut data = form_ctx.get_submit_data(); @@ -178,8 +182,15 @@ impl LoadableComponent for ProxmoxAuthView { fn create(ctx: &LoadableComponentContext) -> Self { let store = Store::new(); - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw)); - Self { store, selection } + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); + Self { + state: LoadableComponentState::new(), + store, + selection, + } } fn load( @@ -201,7 +212,6 @@ impl LoadableComponent for ProxmoxAuthView { let props = ctx.props(); match msg { - Msg::Redraw => true, Msg::Remove => { let Some(info) = self.get_selected_record() else { return true; @@ -218,7 +228,7 @@ impl LoadableComponent for ProxmoxAuthView { return true; }; - let link = ctx.link(); + let link = ctx.link().clone(); link.clone().spawn(async move { if let Err(err) = delete_item(base_url, info.realm.into()).await { link.show_error(tr!("Unable to delete item"), err, true); @@ -319,17 +329,17 @@ impl LoadableComponent for ProxmoxAuthView { .with_child( Button::new(tr!("Edit")) .disabled(edit_disabled) - .onclick(ctx.link().callback(|_| Msg::Edit)), + .onclick(ctx.link().custom_callback(|_| Msg::Edit)), ) .with_child( Button::new(tr!("Remove")) .disabled(remove_disabled) - .onclick(ctx.link().callback(|_| Msg::Remove)), + .onclick(ctx.link().custom_callback(|_| Msg::Remove)), ) .with_child( Button::new(tr!("Sync")) .disabled(sync_disabled) - .onclick(ctx.link().callback(|_| Msg::Sync)), + .onclick(ctx.link().custom_callback(|_| Msg::Sync)), ); Some(toolbar.into()) @@ -341,8 +351,8 @@ impl LoadableComponent for ProxmoxAuthView { .selection(self.selection.clone()) .class("pwt-flex-fit") .on_row_dblclick({ - let link = ctx.link(); - move |_: &mut _| link.send_message(Msg::Edit) + let link = ctx.link().clone(); + move |_: &mut _| link.send_custom_message(Msg::Edit) }) .into() } @@ -398,7 +408,7 @@ impl LoadableComponent for ProxmoxAuthView { .into(), ), ViewState::Sync(realm) => { - let link = ctx.link(); + let link = ctx.link().clone(); let url = format!( "{}/{}/sync", ctx.props().base_url, diff --git a/src/configuration/network_view.rs b/src/configuration/network_view.rs index d7d519c..62b5e2e 100644 --- a/src/configuration/network_view.rs +++ b/src/configuration/network_view.rs @@ -12,7 +12,10 @@ use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader}; use pwt::widget::menu::{Menu, MenuButton, MenuItem}; use pwt::widget::{Button, Column, Container, SplitPane, Toolbar}; -use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster, TaskProgress}; +use crate::{ + LoadableComponent, LoadableComponentContext, LoadableComponentMaster, + LoadableComponentScopeExt, LoadableComponentState, TaskProgress, +}; use proxmox_client::ApiResponseData; use crate::percent_encoding::percent_encode_component; @@ -66,12 +69,15 @@ impl NetworkView { #[doc(hidden)] pub struct ProxmoxNetworkView { + state: LoadableComponentState, columns: Rc>>, store: Store, changes: String, selection: Selection, } +crate::impl_deref_mut_property!(ProxmoxNetworkView, state, LoadableComponentState); + #[derive(PartialEq)] pub enum ViewState { AddBridge, @@ -81,7 +87,6 @@ pub enum ViewState { } pub enum Msg { - SelectionChange, RemoveItem, Changes(String), RevertChanges, @@ -119,19 +124,23 @@ impl LoadableComponent for ProxmoxNetworkView { ctx: &LoadableComponentContext, ) -> Pin>>> { let store = self.store.clone(); - let link = ctx.link(); + let link = ctx.link().clone(); Box::pin(async move { let (data, changes) = load_interfaces().await?; store.write().set_data(data); - link.send_message(Msg::Changes(changes)); + link.send_custom_message(Msg::Changes(changes)); Ok(()) }) } fn create(ctx: &LoadableComponentContext) -> Self { let store = Store::with_extract_key(|record: &Interface| Key::from(record.name.as_str())); - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::SelectionChange)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); Self { + state: LoadableComponentState::new(), store, selection, changes: String::new(), @@ -141,10 +150,9 @@ impl LoadableComponent for ProxmoxNetworkView { fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { match msg { - Msg::SelectionChange => true, Msg::RemoveItem => { if let Some(key) = self.selection.selected_key() { - let link = ctx.link(); + let link = ctx.link().clone(); link.clone().spawn(async move { if let Err(err) = delete_interface(key).await { link.show_error(tr!("Unable to delete item"), err, true); @@ -159,8 +167,8 @@ impl LoadableComponent for ProxmoxNetworkView { true } Msg::RevertChanges => { - let link = ctx.link(); - link.clone().spawn(async move { + let link = ctx.link().clone(); + self.spawn(async move { if let Err(err) = revert_changes().await { link.show_error(tr!("Unable to revert changes"), err, true); } @@ -169,7 +177,7 @@ impl LoadableComponent for ProxmoxNetworkView { false } Msg::ApplyChanges => { - let link = ctx.link(); + let link = ctx.link().clone(); link.clone().spawn(async move { match apply_changes().await { Err(err) => { @@ -219,7 +227,7 @@ impl LoadableComponent for ProxmoxNetworkView { .with_child( Button::new(tr!("Revert")) .disabled(no_changes) - .onclick(link.callback(|_| Msg::RevertChanges)), + .onclick(link.custom_callback(|_| Msg::RevertChanges)), ) .with_child( Button::new(tr!("Edit")) @@ -229,18 +237,18 @@ impl LoadableComponent for ProxmoxNetworkView { .with_child( Button::new(tr!("Remove")) .disabled(disabled) - .onclick(link.callback(|_| Msg::RemoveItem)), + .onclick(link.custom_callback(|_| Msg::RemoveItem)), ) .with_spacer() .with_child( Button::new(tr!("Apply Configuration")) .disabled(no_changes) - .onclick(link.callback(|_| Msg::ApplyChanges)), + .onclick(link.custom_callback(|_| Msg::ApplyChanges)), ) .with_flex_spacer() .with_child({ - let loading = ctx.loading(); - let link = ctx.link(); + let loading = self.loading(); + let link = ctx.link().clone(); Button::refresh(loading).onclick(move |_| link.send_reload()) }); @@ -248,7 +256,7 @@ impl LoadableComponent for ProxmoxNetworkView { } fn main_view(&self, ctx: &LoadableComponentContext) -> Html { - let link = ctx.link(); + let link = ctx.link().clone(); let table = DataTable::new(Rc::clone(&self.columns), self.store.clone()) .class("pwt-flex-fit") diff --git a/src/configuration/pve/lxc_network_panel.rs b/src/configuration/pve/lxc_network_panel.rs index 953edba..3527b1e 100644 --- a/src/configuration/pve/lxc_network_panel.rs +++ b/src/configuration/pve/lxc_network_panel.rs @@ -23,9 +23,12 @@ use crate::form::pve::lxc_network_property; use crate::form::typed_load; use crate::{ configuration::guest_config_url, form::pve::PveGuestType, LoadableComponent, - LoadableComponentContext, + LoadableComponentContext, LoadableComponentScopeExt, +}; +use crate::{ + http_put, impl_deref_mut_property, ConfirmButton, LoadableComponentMaster, + LoadableComponentState, PropertyEditDialog, }; -use crate::{http_put, ConfirmButton, LoadableComponentMaster, PropertyEditDialog}; #[derive(Clone, PartialEq, Properties)] #[builder] @@ -82,21 +85,23 @@ pub enum ViewState { } pub enum Msg { - Redraw, SelectionChange, Remove(Key), } pub struct LxcNetworkComp { + state: LoadableComponentState, columns: Rc>>, store: Store, selection: Selection, } +impl_deref_mut_property!(LxcNetworkComp, state, LoadableComponentState); + impl LxcNetworkComp { fn edit_dialog(&self, ctx: &LoadableComponentContext, name: Option) -> Html { let props = ctx.props(); - let link = ctx.link(); + let link = ctx.link().clone(); let property = lxc_network_property( Some(props.node.clone()), @@ -156,12 +161,17 @@ impl LoadableComponent for LxcNetworkComp { } fn create(ctx: &LoadableComponentContext) -> Self { - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::SelectionChange)); - let store = Store::new().on_change(ctx.link().callback(|_| Msg::Redraw)); + let selection = + Selection::new().on_select(ctx.link().custom_callback(|_| Msg::SelectionChange)); + let store = Store::new().on_change({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); ctx.link().repeated_load(3000); Self { + state: LoadableComponentState::new(), store, selection, columns: columns(), @@ -170,15 +180,14 @@ impl LoadableComponent for LxcNetworkComp { fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { let props = ctx.props(); - let link = ctx.link(); + let link = ctx.link().clone(); match msg { Msg::SelectionChange => true, - Msg::Redraw => true, Msg::Remove(key) => { let url = guest_config_url(props.vmid, &props.node, &props.remote, PveGuestType::Lxc); - link.clone().spawn(async move { + self.spawn(async move { let param = json!({ "delete": [ key.to_string() ]}); let result: Result<(), _> = crate::http_put(&url, Some(param)).await; if let Err(err) = result { @@ -238,7 +247,7 @@ impl LoadableComponent for LxcNetworkComp { let key = selected_key.clone(); move |_| { if let Some(key) = &key { - link.send_message(Msg::Remove(key.clone())) + link.send_custom_message(Msg::Remove(key.clone())) } } }) @@ -259,7 +268,7 @@ impl LoadableComponent for LxcNetworkComp { fn main_view(&self, ctx: &LoadableComponentContext) -> Html { let props = ctx.props(); let readonly = props.readonly; - let link = ctx.link(); + let link = ctx.link().clone(); if props.mobile { let mut tiles = Vec::new(); diff --git a/src/lib.rs b/src/lib.rs index 6df8a32..f4761a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,7 +88,8 @@ pub mod layout; mod loadable_component; pub use loadable_component::{ - LoadableComponent, LoadableComponentContext, LoadableComponentLink, LoadableComponentMaster, + LoadableComponent, LoadableComponentContext, LoadableComponentMaster, LoadableComponentScope, + LoadableComponentScopeExt, LoadableComponentState, }; mod node_info; @@ -336,3 +337,33 @@ pub fn available_language_list() -> Vec { ), ] } + +#[cfg(doc)] +use std::ops::{Deref, DerefMut}; + +/// Implement [Deref] to a structure member. +#[macro_export] +macro_rules! impl_deref_property { + ($ty:ty, $property_name:ident, $property_type:ty) => { + impl std::ops::Deref for $ty { + type Target = $property_type; + + fn deref(&self) -> &Self::Target { + &self.$property_name + } + } + }; +} + +/// Implement [DerefMut] to a structure member. +#[macro_export] +macro_rules! impl_deref_mut_property { + ($ty:ty, $property_name:ident, $property_type:ty) => { + $crate::impl_deref_property!($ty, $property_name, $property_type); + impl std::ops::DerefMut for $ty { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.$property_name + } + } + }; +} diff --git a/src/loadable_component.rs b/src/loadable_component.rs index f0e28a9..43cb7a9 100644 --- a/src/loadable_component.rs +++ b/src/loadable_component.rs @@ -1,163 +1,313 @@ -use anyhow::Error; -use serde_json::Value; use std::future::Future; +use std::ops::DerefMut; use std::pin::Pin; -use yew_router::scope_ext::RouterScopeExt; +use anyhow::Error; use gloo_timers::callback::Timeout; +use serde_json::Value; use yew::html::Scope; use pwt::dom::DomVisibilityObserver; use pwt::prelude::*; -use pwt::state::NavigationContextExt; use pwt::widget::{AlertDialog, Column}; use pwt::AsyncPool; +#[cfg(doc)] +use crate::impl_deref_mut_property; +#[cfg(doc)] +use pwt::widget::Dialog; + use crate::{TaskProgress, TaskViewer}; -pub struct LoadableComponentState { - loading: usize, - last_load_error: Option, - repeat_timespan: u32, /* 0 => no repeated loading */ - task_base_url: Option, -} +pub type LoadableComponentContext = Context>; +pub type LoadableComponentScope = Scope>; + +/// Loadable Components +/// +/// - Load data using an async function [LoadableComponent::load] +/// - repeated load possible +/// - pause repeated load when component is not visible (uses [DomVisibilityObserver]) +/// - display the loaded data [LoadableComponent::main_view] +/// - display an optional toolbar [LoadableComponent::toolbar] +/// - display any errors from failed load. +/// - display additional dialogs depening on [LoadableComponent::ViewState] +/// +/// The [LoadableComponentScopeExt] defines available control function on the scope. +/// +/// The [LoadableComponentState] provides acces to load status informations and add the ability +/// to spawn tasks. +/// +/// ``` +/// use proxmox_yew_comp::{LoadableComponent, LoadableComponentState, LoadableComponentContext}; +/// // include the scope extension for (for `change_view`, `send_custom_message`, ...) +/// use proxmox_yew_comp::LoadableComponentScopeExt; +/// # use std::pin::Pin; +/// # use std::rc::Rc; +/// # use std::future::Future; +/// # use pwt::prelude::*; +/// # use proxmox_yew_comp::http_get; +/// # use yew::virtual_dom::{VComp, VNode, Key}; +/// +/// // define the component properties +/// #[derive(Clone, PartialEq, Properties)] +/// pub struct MyComponent { +/// key: Option, +/// /* add whatever you need */ +/// }; +/// +/// // define your view states +/// #[derive(PartialEq)] +/// pub enum ViewState { Add, Edit } +/// +/// // define the component message type +/// pub enum Msg { UpdateData(String) } +/// +/// // define the component state +/// pub struct MyComponentState { +/// // you need to inlucde a LoadableComponentState +/// state: LoadableComponentState, +/// // Add any other data you need +/// loaded_data: Option, +/// } +/// +/// // implement DerefMut +/// proxmox_yew_comp::impl_deref_mut_property!( +/// MyComponentState, +/// state, +/// LoadableComponentState +/// ); +/// +/// impl LoadableComponent for MyComponentState { +/// type Properties = MyComponent; +/// type Message = Msg; // component message type +/// type ViewState = ViewState; +/// +/// fn create(ctx: &LoadableComponentContext) -> Self { +/// Self { +/// state: LoadableComponentState::new(), +/// loaded_data: None, +/// } +/// } +/// +/// fn load( +/// &self, +/// ctx: &LoadableComponentContext, +/// ) -> Pin>>> { +/// let link = ctx.link().clone(); +/// Box::pin(async move { +/// let data = http_get("/something", None).await?; // load something here +/// link.send_custom_message(Msg::UpdateData(data)); +/// Ok(()) +/// }) +/// } +/// +/// fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { +/// match msg { +/// Msg::UpdateData(data) => self.loaded_data = Some(data), +/// } +/// true +/// } +/// +/// fn main_view(&self, ctx: &LoadableComponentContext) -> Html { +/// let text: String = if let Some(data) = &self.loaded_data { +/// data.clone() +/// } else { +/// "no data".into() +/// }; +/// html!{text} +/// } +/// } +/// +/// // add ability to generate the yew Component (provided by [LoadableComponentMaster]) +/// use proxmox_yew_comp::LoadableComponentMaster; +/// impl From for VNode { +/// fn from(props: MyComponent) -> VNode { +/// let key = props.key.clone(); +/// let comp = VComp::new::>(Rc::new(props), key); +/// VNode::from(comp) +/// } +/// } +/// +/// ``` +pub trait LoadableComponent: + Sized + DerefMut> + 'static +{ + /// The yew component properties. + type Properties: Properties; + /// The yew component message type. + type Message: 'static; + /// The view state + /// + /// The view state of the component can be changed with [LoadableComponentScopeExt::change_view]. + /// The value is then passed to the [LoadableComponent::dialog_view] function which can render + /// different dialogs. + type ViewState: 'static + PartialEq; -pub struct LoadableComponentContext<'a, L: LoadableComponent + Sized + 'static> { - ctx: &'a Context>, - comp_state: &'a LoadableComponentState, -} + /// Create a new instance + fn create(ctx: &LoadableComponentContext) -> Self; + + /// Async Load + fn load( + &self, + ctx: &LoadableComponentContext, + ) -> Pin>>>; -impl LoadableComponentContext<'_, L> { - pub fn props(&self) -> &L::Properties { - self.ctx.props() + /// Yew component update function (see [Component::update]) + #[allow(unused_variables)] + fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { + true } - pub fn link(&self) -> LoadableComponentLink { - LoadableComponentLink { - link: self.ctx.link().clone(), - } + + /// Yew component changed function (see [Component::changed]) + #[allow(unused_variables)] + fn changed( + &mut self, + ctx: &LoadableComponentContext, + _old_props: &Self::Properties, + ) -> bool { + true } - pub fn loading(&self) -> bool { - self.comp_state.loading > 0 + + /// Optional toolbar + #[allow(unused_variables)] + fn toolbar(&self, ctx: &LoadableComponentContext) -> Option { + None } - pub fn last_load_errors(&self) -> Option<&str> { - self.comp_state.last_load_error.as_deref() + /// Main view (see [Component::view]) + /// + /// The difference is that we render the result into a [Column], with an optional + /// toolbar on the top. + fn main_view(&self, ctx: &LoadableComponentContext) -> Html; + + /// ViewState dependent dialogs + /// + /// The result is rendered below the main view. Usually some kind of [Dialog] window. + /// + /// The view state can be changed with `link.change_view(..)` and `link.change_view_callback(...)`. + #[allow(unused_variables)] + fn dialog_view( + &self, + ctx: &LoadableComponentContext, + view_state: &Self::ViewState, + ) -> Option { + None } + + /// Yew component rendered function (see [Component::rendered]) + #[allow(unused_variables)] + fn rendered(&mut self, ctx: &LoadableComponentContext, first_render: bool) {} } -pub struct LoadableComponentLink { - link: Scope>, +#[derive(Clone, PartialEq)] +pub enum ViewState { + Main, + /// Show the dialog returned by dialog_view + Dialog(V), + /// Show proxmox api task status + TaskProgress(String), + /// Show proxmox api task log + TaskLog(String, Option), + /// Show an error message dialog + Error(String, String, /* reload_on_close */ bool), } -impl Clone for LoadableComponentLink { - fn clone(&self) -> Self { - Self { - link: self.link.clone(), - } - } +pub enum Msg { + DataChange, + Load, + RepeatedLoad(u32 /* repeat time in miliseconds */), + LoadResult(Result<(), Error>), + ChangeView(/*reload*/ bool, ViewState), + ChildMessage(M), + Visible(bool), + Spawn(Pin>>), } -impl LoadableComponentLink { - pub fn send_message(&self, msg: impl Into) { - let msg = msg.into(); - self.link.send_message(Msg::ChildMessage(msg)); - } +pub trait LoadableComponentScopeExt { + fn send_custom_message(&self, msg: M); + fn send_reload(&self); + fn send_redraw(&self); + fn repeated_load(&self, miliseconds: u32); - pub fn callback(&self, function: F) -> Callback + fn change_view(&self, child_view_state: Option); + fn custom_callback(&self, function: F) -> Callback where - M: Into, - F: Fn(IN) -> M + 'static, - { - self.link.callback(move |p: IN| { - let msg: L::Message = function(p).into(); - Msg::ChildMessage(msg) - }) - } + M: Into, + F: Fn(IN) -> M + 'static; - /// Spawn a future using the [AsyncPool] from the component. - pub fn spawn(&self, future: Fut) + fn change_view_callback(&self, function: F) -> Callback where - Fut: Future + 'static, - { - self.link.send_message(Msg::Spawn(Box::pin(future))); - } + C: Into>, + F: Fn(IN) -> C + 'static; - pub fn send_future(&self, future: Fut) + /// Spawn a future using the [AsyncPool] from the component. + fn spawn(&self, future: Fut) where - M: Into, - Fut: Future + 'static, - { - let link = self.link.clone(); - self.link.send_message(Msg::Spawn(Box::pin(async move { - let message: L::Message = future.await.into(); - link.send_message(Msg::ChildMessage(message)); - }))); - } + Fut: Future + 'static; - pub fn callback_future(&self, function: F) -> Callback - where - M: Into, - Fut: Future + 'static, - F: Fn(IN) -> Fut + 'static, - { - let link = self.clone(); + fn show_error( + &self, + title: impl Into, + msg: impl std::fmt::Display, + reload_on_close: bool, + ); - let closure = move |input: IN| { - link.send_future(function(input)); - }; + fn show_task_progres(&self, task_id: impl Into); - closure.into() - } + fn show_task_log(&self, task_id: impl Into, endtime: Option); - pub fn send_reload(&self) { - self.link.send_message(Msg::Load) - } + fn start_task(&self, command_path: impl Into, data: Option, short: bool); +} - pub fn repeated_load(&self, miliseconds: u32) { - self.link.send_message(Msg::RepeatedLoad(miliseconds)); +impl> + LoadableComponentScopeExt for Scope> +{ + fn send_custom_message(&self, msg: M) { + self.send_message(Msg::ChildMessage(msg)); } - pub fn task_base_url(&self, base_url: impl Into) { - self.link.send_message(Msg::TaskBaseUrl(base_url.into())); + fn send_reload(&self) { + self.send_message(Msg::Load); } - pub fn show_error( - &self, - title: impl Into, - msg: impl std::fmt::Display, - reload_on_close: bool, - ) { - let view_state = ViewState::Error(title.into(), msg.to_string(), reload_on_close); - self.link.send_message(Msg::ChangeView(false, view_state)); + fn send_redraw(&self) { + self.send_message(Msg::DataChange); } - pub fn show_task_progres(&self, task_id: impl Into) { - let view_state = ViewState::TaskProgress(task_id.into()); - self.link.send_message(Msg::ChangeView(false, view_state)); + fn repeated_load(&self, miliseconds: u32) { + self.send_message(Msg::RepeatedLoad(miliseconds)); } - pub fn show_task_log(&self, task_id: impl Into, endtime: Option) { - let view_state = ViewState::TaskLog(task_id.into(), endtime); - self.link.send_message(Msg::ChangeView(false, view_state)); + fn custom_callback(&self, function: F) -> Callback + where + M: Into, + F: Fn(IN) -> M + 'static, + { + let scope = self.clone(); + let closure = move |input| { + let output = function(input); + scope.send_custom_message(output); + }; + Callback::from(closure) } - pub fn change_view(&self, child_view_state: Option) { + fn change_view(&self, child_view_state: Option) { let view_state = if let Some(child_view_state) = child_view_state { ViewState::Dialog(child_view_state) } else { ViewState::Main }; - self.link.send_message(Msg::ChangeView(false, view_state)); + self.send_message(Msg::ChangeView(false, view_state)); } - pub fn change_view_callback(&self, function: F) -> Callback + fn change_view_callback(&self, function: F) -> Callback where - M: Into>, - F: Fn(IN) -> M + 'static, + C: Into>, + F: Fn(IN) -> C + 'static, { - self.link.callback(move |p: IN| { - let state: Option = function(p).into(); + self.callback(move |p: IN| { + let state: Option = function(p).into(); if let Some(state) = state { Msg::ChangeView(true, ViewState::Dialog(state)) } else { @@ -166,11 +316,38 @@ impl LoadableComponentLink { }) } - pub fn start_task(&self, command_path: impl Into, data: Option, short: bool) { + fn spawn(&self, future: Fut) + where + Fut: Future + 'static, + { + self.send_message(Msg::Spawn(Box::pin(future))); + } + + fn show_error( + &self, + title: impl Into, + msg: impl std::fmt::Display, + reload_on_close: bool, + ) { + let view_state = ViewState::Error(title.into(), msg.to_string(), reload_on_close); + self.send_message(Msg::ChangeView(false, view_state)); + } + + fn show_task_progres(&self, task_id: impl Into) { + let view_state = ViewState::TaskProgress(task_id.into()); + self.send_message(Msg::ChangeView(false, view_state)); + } + + fn show_task_log(&self, task_id: impl Into, endtime: Option) { + let view_state = ViewState::TaskLog(task_id.into(), endtime); + self.send_message(Msg::ChangeView(false, view_state)); + } + + fn start_task(&self, command_path: impl Into, data: Option, short: bool) { let command_path: String = command_path.into(); let link = self.clone(); let command_future = crate::http_post::(command_path, data); - self.link.send_message(Msg::Spawn(Box::pin(async move { + self.send_message(Msg::Spawn(Box::pin(async move { match command_future.await { Ok(task_id) => { link.send_reload(); @@ -187,140 +364,86 @@ impl LoadableComponentLink { } }))); } - - /// Returns the original [`yew::html::Scope`] of the master component. - /// - /// This is useful when e.g. trying to get an higher level context - pub fn yew_link(&self) -> &Scope> { - &self.link - } } -impl RouterScopeExt for LoadableComponentLink { - fn navigator(&self) -> Option { - self.link.navigator() - } - - fn location(&self) -> Option { - self.link.location() - } - - fn route(&self) -> Option - where - R: yew_router::Routable + 'static, - { - self.link.route() - } - - fn add_location_listener( - &self, - cb: Callback, - ) -> Option { - self.link.add_location_listener(cb) - } - - fn add_navigator_listener( - &self, - cb: Callback, - ) -> Option { - self.link.add_navigator_listener(cb) - } +/// Base state for [LoadableComponent] implementations. +/// +/// The struct provides the following features: +/// +/// - access to load status informations +/// - setup task base url +/// - spawn tasks: includes an [AsyncPool], so that any [LoadableComponent] can spawn +/// task via this pool. +/// +/// The [LoadableComponent] trait requires access to this struct via [DerefMut]. The +/// macro [impl_deref_mut_property] provides an easy way to +/// implement that. +/// +/// ``` +/// use proxmox_yew_comp::LoadableComponentState; +/// # #[derive(PartialEq)] +/// # pub enum ViewState { Add, Edit } +/// pub struct MyComponentState { +/// state: LoadableComponentState, +/// // Add any other data you need +/// other_data: String, +/// } +/// // implement DerefMut +/// proxmox_yew_comp::impl_deref_mut_property!(MyComponentState, state, LoadableComponentState); +/// ``` +pub struct LoadableComponentState { + loading: usize, + last_load_error: Option, + repeat_timespan: u32, /* 0 => no repeated loading */ + task_base_url: Option, + view_state: ViewState, + reload_timeout: Option, + visible: bool, + visibitlity_observer: Option, + node_ref: NodeRef, + async_pool: AsyncPool, } -impl NavigationContextExt for LoadableComponentLink { - fn nav_context(&self) -> Option { - self.link.nav_context() - } - - fn full_path(&self) -> Option { - self.link.full_path() - } - - fn push_relative_route(&self, path: &str) { - self.link.push_relative_route(path) +impl LoadableComponentState { + pub fn new() -> Self { + Self { + loading: 0, + last_load_error: None, + repeat_timespan: 0, + task_base_url: None, + view_state: ViewState::Main, + reload_timeout: None, + visible: true, + visibitlity_observer: None, + node_ref: NodeRef::default(), + async_pool: AsyncPool::new(), + } } -} - -pub trait LoadableComponent: Sized { - type Properties: Properties; - type Message: 'static; - type ViewState: 'static + PartialEq; - fn create(ctx: &LoadableComponentContext) -> Self; - - fn load( - &self, - ctx: &LoadableComponentContext, - ) -> Pin>>>; - - #[allow(unused_variables)] - fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { - true + pub fn loading(&self) -> bool { + self.loading > 0 } - #[allow(unused_variables)] - fn changed( - &mut self, - ctx: &LoadableComponentContext, - _old_props: &Self::Properties, - ) -> bool { - true + pub fn last_load_errors(&self) -> Option<&str> { + self.last_load_error.as_deref() } - #[allow(unused_variables)] - fn toolbar(&self, ctx: &LoadableComponentContext) -> Option { - None + pub fn set_task_base_url(&mut self, base_url: AttrValue) { + self.task_base_url = Some(base_url); } - fn main_view(&self, ctx: &LoadableComponentContext) -> Html; - - #[allow(unused_variables)] - fn dialog_view( - &self, - ctx: &LoadableComponentContext, - view_state: &Self::ViewState, - ) -> Option { - None + /// Spawn a future using the [AsyncPool] from the component. + pub fn spawn(&self, future: Fut) + where + Fut: Future + 'static, + { + self.async_pool.spawn(future); } - - #[allow(unused_variables)] - fn rendered(&mut self, ctx: &LoadableComponentContext, first_render: bool) {} -} - -#[derive(Clone, PartialEq)] -pub enum ViewState { - Main, - /// Show the dialog returned by dialog_view - Dialog(V), - /// Show proxmox api task status - TaskProgress(String), - /// Show proxmox api task log - TaskLog(String, Option), - /// Show an error message dialog - Error(String, String, /* reload_on_close */ bool), -} - -pub enum Msg { - DataChange, - Load, - RepeatedLoad(u32 /* repeat time in miliseconds */), - LoadResult(Result<(), Error>), - ChangeView(/*reload*/ bool, ViewState), - ChildMessage(M), - TaskBaseUrl(AttrValue), - Visible(bool), - Spawn(Pin>>), } +#[doc(hidden)] pub struct LoadableComponentMaster { state: L, - comp_state: LoadableComponentState, - view_state: ViewState, - reload_timeout: Option, - visible: bool, - visibitlity_observer: Option, - node_ref: NodeRef, - async_pool: AsyncPool, } impl Component for LoadableComponentMaster { @@ -328,91 +451,65 @@ impl Component for LoadableComponentMaster { type Properties = L::Properties; fn create(ctx: &Context) -> Self { - let loading = 0; - - let comp_state = LoadableComponentState { - loading, - last_load_error: None, - repeat_timespan: 0, - task_base_url: None, - }; - - let sub_context = LoadableComponentContext { - ctx, - comp_state: &comp_state, - }; - // Send Msg::Load first (before any Msg::RepeatedLoad in create), so that we // can avoid multiple loads at startup ctx.link().send_message(Msg::Load); - let state = L::create(&sub_context); + let mut state = L::create(ctx); + state.visible = true; - Self { - state, - comp_state, - view_state: ViewState::Main, - reload_timeout: None, - visible: true, - visibitlity_observer: None, - node_ref: NodeRef::default(), - async_pool: AsyncPool::new(), - } + Self { state } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { Msg::Spawn(future) => { - self.async_pool.spawn(future); + self.state.async_pool.spawn(future); false } Msg::DataChange => true, Msg::Load => { - self.comp_state.loading += 1; + let load_future = self.state.load(ctx); + self.state.loading += 1; let link = ctx.link().clone(); - let sub_context = LoadableComponentContext { - ctx, - comp_state: &self.comp_state, - }; - let load_future = self.state.load(&sub_context); - self.async_pool.spawn(async move { + self.state.async_pool.spawn(async move { let data = load_future.await; link.send_message(Msg::LoadResult(data)); }); true } Msg::RepeatedLoad(timespan) => { - self.comp_state.repeat_timespan = timespan; - self.reload_timeout = None; - if self.comp_state.loading == 0 { + self.state.repeat_timespan = timespan; + self.state.reload_timeout = None; + if self.state.loading == 0 { ::update(self, ctx, Msg::Load); } false } Msg::LoadResult(data) => { - self.comp_state.loading -= 1; + self.state.loading -= 1; match data { Ok(()) => { - self.comp_state.last_load_error = None; + self.state.last_load_error = None; } Err(err) => { - let this_is_the_first_error = self.comp_state.last_load_error.is_none(); - self.comp_state.last_load_error = Some(err.to_string()); + let this_is_the_first_error = self.state.last_load_error.is_none(); + self.state.last_load_error = Some(err.to_string()); if this_is_the_first_error { - self.view_state = + self.state.view_state = ViewState::Error(tr!("Load failed"), err.to_string(), false); } } } - self.reload_timeout = None; - if self.comp_state.loading == 0 { + self.state.reload_timeout = None; + if self.state.loading == 0 { /* no outstanding loads */ - if self.comp_state.repeat_timespan > 0 { + if self.state.repeat_timespan > 0 { let link = ctx.link().clone(); - if self.visible { - self.reload_timeout = - Some(Timeout::new(self.comp_state.repeat_timespan, move || { + if self.state.visible { + self.state.reload_timeout = + Some(Timeout::new(self.state.repeat_timespan, move || { link.send_message(Msg::Load); })); } @@ -421,7 +518,7 @@ impl Component for LoadableComponentMaster { true } Msg::ChangeView(reload_data, view_state) => { - if self.view_state == view_state { + if self.state.view_state == view_state { return false; } @@ -429,29 +526,21 @@ impl Component for LoadableComponentMaster { ctx.link().send_message(Msg::Load); } - self.view_state = view_state; + self.state.view_state = view_state; true } Msg::ChildMessage(child_msg) => { - let sub_context = LoadableComponentContext { - ctx, - comp_state: &self.comp_state, - }; - self.state.update(&sub_context, child_msg); + self.state.update(ctx, child_msg); true } - Msg::TaskBaseUrl(base_url) => { - self.comp_state.task_base_url = Some(base_url); - false - } Msg::Visible(visible) => { - if self.visible == visible { + if self.state.visible == visible { return false; } - self.visible = visible; - if self.comp_state.loading == 0 && self.visible { + self.state.visible = visible; + if self.state.loading == 0 && self.state.visible { /* no outstanding loads */ - if self.comp_state.loading == 0 { + if self.state.loading == 0 { ::update(self, ctx, Msg::Load); } } @@ -461,26 +550,16 @@ impl Component for LoadableComponentMaster { } fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { - let sub_context = LoadableComponentContext { - ctx, - comp_state: &self.comp_state, - }; - - self.state.changed(&sub_context, _old_props) + self.state.changed(ctx, _old_props) } fn view(&self, ctx: &Context) -> Html { - let sub_context = LoadableComponentContext { - ctx, - comp_state: &self.comp_state, - }; - - let main_view = self.state.main_view(&sub_context); + let main_view = self.state.main_view(ctx); let dialog: Option = - match &self.view_state { + match &self.state.view_state { ViewState::Main => None, - ViewState::Dialog(view_state) => self.state.dialog_view(&sub_context, view_state), + ViewState::Dialog(view_state) => self.state.dialog_view(ctx, view_state), ViewState::Error(title, msg, reload_on_close) => { let reload_on_close = *reload_on_close; Some( @@ -498,7 +577,7 @@ impl Component for LoadableComponentMaster { .callback(move |_| Msg::ChangeView(true, ViewState::Main)), ); - if let Some(base_url) = &self.comp_state.task_base_url { + if let Some(base_url) = &self.state.task_base_url { task_progress.set_base_url(base_url); } @@ -510,19 +589,19 @@ impl Component for LoadableComponentMaster { .callback(move |_| Msg::ChangeView(true, ViewState::Main)), ); - if let Some(base_url) = &self.comp_state.task_base_url { + if let Some(base_url) = &self.state.task_base_url { task_viewer.set_base_url(base_url); } Some(task_viewer.into()) } }; - let toolbar = self.state.toolbar(&sub_context); + let toolbar = self.state.toolbar(ctx); let mut alert_msg = None; if dialog.is_none() { - if let Some(msg) = &self.comp_state.last_load_error { + if let Some(msg) = &self.state.last_load_error { alert_msg = Some(pwt::widget::error_message(msg).class("pwt-border-top")); } } @@ -533,23 +612,18 @@ impl Component for LoadableComponentMaster { .with_child(main_view) .with_optional_child(alert_msg) .with_optional_child(dialog) - .into_html_with_ref(self.node_ref.clone()) + .into_html_with_ref(self.state.node_ref.clone()) } fn rendered(&mut self, ctx: &Context, first_render: bool) { - if self.visibitlity_observer.is_none() && self.reload_timeout.is_some() { - if let Some(el) = self.node_ref.cast::() { - self.visibitlity_observer = Some(DomVisibilityObserver::new( + if self.state.visibitlity_observer.is_none() && self.state.reload_timeout.is_some() { + if let Some(el) = self.state.node_ref.cast::() { + self.state.visibitlity_observer = Some(DomVisibilityObserver::new( &el, ctx.link().callback(Msg::Visible), )) } } - let sub_context = LoadableComponentContext { - ctx, - comp_state: &self.comp_state, - }; - - self.state.rendered(&sub_context, first_render); + self.state.rendered(ctx, first_render); } } diff --git a/src/node_status_panel.rs b/src/node_status_panel.rs index 3f8946d..9249c01 100644 --- a/src/node_status_panel.rs +++ b/src/node_status_panel.rs @@ -17,7 +17,7 @@ use proxmox_node_status::{NodePowerCommand, NodeStatus}; use crate::utils::copy_text_to_clipboard; use crate::{ http_get, http_post, node_info, ConfirmButton, LoadableComponent, LoadableComponentContext, - LoadableComponentMaster, + LoadableComponentMaster, LoadableComponentScopeExt, LoadableComponentState, }; #[derive(Properties, Clone, PartialEq)] @@ -53,7 +53,6 @@ enum Msg { Error(Error), Loaded(Rc), RebootOrShutdown(NodePowerCommand), - Reload, } #[derive(PartialEq)] @@ -62,10 +61,17 @@ enum ViewState { } struct ProxmoxNodeStatusPanel { + state: LoadableComponentState, node_status: Option>, error: Option, } +crate::impl_deref_mut_property!( + ProxmoxNodeStatusPanel, + state, + LoadableComponentState +); + impl ProxmoxNodeStatusPanel { fn change_power_state(&self, ctx: &LoadableComponentContext, command: NodePowerCommand) { let Some(url) = ctx.props().status_base_url.clone() else { @@ -79,8 +85,8 @@ impl ProxmoxNodeStatusPanel { })); match http_post(url.as_str(), data).await { - Ok(()) => link.send_message(Msg::Reload), - Err(err) => link.send_message(Msg::Error(err)), + Ok(()) => link.send_redraw(), + Err(err) => link.send_custom_message(Msg::Error(err)), } }); } @@ -90,8 +96,8 @@ impl ProxmoxNodeStatusPanel { ctx: &LoadableComponentContext, fingerprint: &str, ) -> Dialog { - let link = ctx.link(); - let link_button = ctx.link(); + let link = ctx.link().clone(); + let link_button = ctx.link().clone(); let fingerprint = fingerprint.to_owned(); Dialog::new(tr!("Fingerprint")) @@ -135,10 +141,10 @@ impl LoadableComponent for ProxmoxNodeStatusPanel { type ViewState = ViewState; type Properties = NodeStatusPanel; - fn create(ctx: &crate::LoadableComponentContext) -> Self { + fn create(ctx: &LoadableComponentContext) -> Self { ctx.link().repeated_load(5000); - Self { + state: LoadableComponentState::new(), node_status: None, error: None, } @@ -146,7 +152,7 @@ impl LoadableComponent for ProxmoxNodeStatusPanel { fn load( &self, - ctx: &crate::LoadableComponentContext, + ctx: &LoadableComponentContext, ) -> std::pin::Pin>>> { let url = ctx.props().status_base_url.clone(); let link = ctx.link().clone(); @@ -154,15 +160,15 @@ impl LoadableComponent for ProxmoxNodeStatusPanel { Box::pin(async move { if let Some(url) = url { match http_get(url.as_str(), None).await { - Ok(res) => link.send_message(Msg::Loaded(Rc::new(res))), - Err(err) => link.send_message(Msg::Error(err)), + Ok(res) => link.send_custom_message(Msg::Loaded(Rc::new(res))), + Err(err) => link.send_custom_message(Msg::Error(err)), } } Ok(()) }) } - fn update(&mut self, ctx: &crate::LoadableComponentContext, msg: Self::Message) -> bool { + fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { match msg { Msg::Error(err) => { self.error = Some(err); @@ -177,7 +183,6 @@ impl LoadableComponent for ProxmoxNodeStatusPanel { self.change_power_state(ctx, command); false } - Msg::Reload => true, } } @@ -197,7 +202,7 @@ impl LoadableComponent for ProxmoxNodeStatusPanel { None } - fn main_view(&self, ctx: &crate::LoadableComponentContext) -> Html { + fn main_view(&self, ctx: &LoadableComponentContext) -> Html { let status = self .node_status .as_ref() @@ -223,7 +228,7 @@ impl LoadableComponent for ProxmoxNodeStatusPanel { .confirm_message(tr!("Are you sure you want to reboot the node?")) .on_activate( ctx.link() - .callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Reboot)), + .custom_callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Reboot)), ) .icon_class("fa fa-undo"), ); @@ -232,7 +237,7 @@ impl LoadableComponent for ProxmoxNodeStatusPanel { .confirm_message(tr!("Are you sure you want to shut down the node?")) .on_activate( ctx.link() - .callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Shutdown)), + .custom_callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Shutdown)), ) .icon_class("fa fa-power-off"), ); diff --git a/src/notes_view.rs b/src/notes_view.rs index 6b0a379..9c10c00 100644 --- a/src/notes_view.rs +++ b/src/notes_view.rs @@ -17,7 +17,7 @@ use proxmox_client::ApiResponseData; use crate::{ ApiLoadCallback, EditWindow, LoadableComponent, LoadableComponentContext, - LoadableComponentMaster, Markdown, + LoadableComponentMaster, LoadableComponentScopeExt, LoadableComponentState, Markdown, }; #[derive(Serialize, Deserialize, Clone, PartialEq)] @@ -107,10 +107,13 @@ pub enum Msg { #[doc(hidden)] pub struct ProxmoxNotesView { + state: LoadableComponentState, data: NotesWithDigest, edit_window_loader: ApiLoadCallback, } +crate::impl_deref_mut_property!(ProxmoxNotesView, state, LoadableComponentState); + impl LoadableComponent for ProxmoxNotesView { type Properties = NotesView; type Message = Msg; @@ -130,6 +133,7 @@ impl LoadableComponent for ProxmoxNotesView { } }); Self { + state: LoadableComponentState::new(), data: NotesWithDigest { notes: String::new(), digest: None, @@ -143,12 +147,12 @@ impl LoadableComponent for ProxmoxNotesView { ctx: &LoadableComponentContext, ) -> Pin>>> { let loader = ctx.props().loader.clone(); - let link = ctx.link(); + let link = ctx.link().clone(); Box::pin(async move { let resp = loader.apply().await?; let notes = resp.data; let digest = resp.attribs.get("digest").cloned(); - link.send_message(Msg::Load(NotesWithDigest { notes, digest })); + link.send_custom_message(Msg::Load(NotesWithDigest { notes, digest })); Ok(()) }) } @@ -169,10 +173,8 @@ impl LoadableComponent for ProxmoxNotesView { .class("pwt-overflow-hidden") .class("pwt-border-bottom") .with_child( - Button::new(tr!("Edit")).on_activate( - ctx.link() - .change_view_callback(|_| Some(ViewState::EditNotes)), - ), + Button::new(tr!("Edit")) + .on_activate(ctx.link().change_view_callback(|_| ViewState::EditNotes)), ) .into(), ) diff --git a/src/object_grid.rs b/src/object_grid.rs index a6313d0..e2321d0 100644 --- a/src/object_grid.rs +++ b/src/object_grid.rs @@ -8,7 +8,7 @@ use indexmap::IndexMap; use proxmox_client::ApiResponseData; use serde_json::Value; -use yew::html::IntoPropValue; +use yew::html::{IntoPropValue, Scope}; use yew::prelude::*; use yew::virtual_dom::{Key, VComp, VNode}; @@ -20,8 +20,11 @@ use pwt::widget::form::FormContext; use pwt::widget::{Button, Toolbar}; use crate::{ApiLoadCallback, IntoApiLoadCallback}; -use crate::{EditWindow, KVGrid, KVGridRow, LoadableComponentLink}; -use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster}; +use crate::{EditWindow, KVGrid, KVGridRow}; +use crate::{ + LoadableComponent, LoadableComponentContext, LoadableComponentMaster, + LoadableComponentScopeExt, LoadableComponentState, +}; use pwt_macros::builder; @@ -257,6 +260,8 @@ pub enum ViewState { #[doc(hidden)] pub struct PwtObjectGrid { + state: LoadableComponentState, + selection: Option, data: Rc, @@ -266,6 +271,8 @@ pub struct PwtObjectGrid { controller_observer: Option>>, } +crate::impl_deref_mut_property!(PwtObjectGrid, state, LoadableComponentState); + impl PwtObjectGrid { fn update_rows(&mut self, props: &ObjectGrid) { let mut rows = Vec::new(); @@ -280,7 +287,11 @@ impl PwtObjectGrid { self.rows = Rc::new(rows); } - fn update_controller(&mut self, props: &ObjectGrid, link: LoadableComponentLink) { + fn update_controller( + &mut self, + props: &ObjectGrid, + link: Scope>, + ) { match &props.controller { None => self.controller_observer = None, Some(controller) => { @@ -292,7 +303,7 @@ impl PwtObjectGrid { guard.split_off(0) }; for command in commands { - link.send_message(Msg::ControllerCommand(command)); + link.send_custom_message(Msg::ControllerCommand(command)); } }, )); @@ -335,6 +346,7 @@ impl LoadableComponent for PwtObjectGrid { ctx.link().repeated_load(3000); let mut me = Self { + state: LoadableComponentState::new(), data: Rc::new(Value::Null), rows: Rc::new(Vec::new()), editors: IndexMap::new(), @@ -342,7 +354,7 @@ impl LoadableComponent for PwtObjectGrid { controller_observer: None, }; me.update_rows(props); - me.update_controller(props, ctx.link()); + me.update_controller(props, ctx.link().clone()); me } @@ -352,12 +364,12 @@ impl LoadableComponent for PwtObjectGrid { ) -> Pin>>> { let props = ctx.props(); let loader = props.loader.clone(); - let link = ctx.link(); + let link = ctx.link().clone(); Box::pin(async move { if let Some(loader) = &loader { let api_resp: ApiResponseData = loader.apply().await?; - link.send_message(Msg::DataChange(api_resp)); + link.send_custom_message(Msg::DataChange(api_resp)); } Ok(()) }) @@ -418,7 +430,7 @@ impl LoadableComponent for PwtObjectGrid { let mut toolbar = Toolbar::new() .border_bottom(true) .with_child(Button::new("Edit").disabled(disable_edit).onclick({ - let link = ctx.link(); + let link = ctx.link().clone(); move |_| { link.change_view(Some(ViewState::EditObject)); } @@ -437,11 +449,11 @@ impl LoadableComponent for PwtObjectGrid { .class("pwt-flex-fit") .rows(Rc::clone(&self.rows)) .data(self.data.clone()) - .on_select(ctx.link().callback(Msg::Select)) + .on_select(ctx.link().custom_callback(Msg::Select)) .on_row_dblclick({ let link = ctx.link().clone(); move |event: &mut DataTableMouseEvent| { - link.send_message(Msg::Select(Some(event.record_key.clone()))); + link.send_custom_message(Msg::Select(Some(event.record_key.clone()))); link.change_view(Some(ViewState::EditObject)); } }) @@ -449,7 +461,7 @@ impl LoadableComponent for PwtObjectGrid { let link = ctx.link().clone(); move |event: &mut DataTableKeyboardEvent| { if event.key() == " " { - link.send_message(Msg::Select(Some(event.record_key.clone()))); + link.send_custom_message(Msg::Select(Some(event.record_key.clone()))); link.change_view(Some(ViewState::EditObject)); } } diff --git a/src/permission_panel.rs b/src/permission_panel.rs index 3ed07b1..e7d2c27 100644 --- a/src/permission_panel.rs +++ b/src/permission_panel.rs @@ -18,7 +18,7 @@ use pwt::widget::data_table::{ use pwt_macros::builder; -use crate::{http_get, LoadableComponent, LoadableComponentMaster}; +use crate::{http_get, LoadableComponent, LoadableComponentMaster, LoadableComponentState}; #[derive(Clone, PartialEq, Properties)] #[builder] @@ -46,10 +46,13 @@ impl PermissionPanel { } } pub struct ProxmoxPermissionPanel { + state: LoadableComponentState<()>, store: TreeStore, columns: Rc>>, } +crate::impl_deref_mut_property!(ProxmoxPermissionPanel, state, LoadableComponentState<()>); + #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] enum PermissionInfo { Permission(String, String, bool), @@ -113,15 +116,19 @@ impl LoadableComponent for ProxmoxPermissionPanel { type Message = (); type ViewState = (); - fn create(_ctx: &crate::LoadableComponentContext) -> Self { + fn create(_ctx: &Context>) -> Self { let store = TreeStore::new(); let columns = Rc::new(columns(&store)); - Self { store, columns } + Self { + state: LoadableComponentState::new(), + store, + columns, + } } fn load( &self, - ctx: &crate::LoadableComponentContext, + ctx: &Context>, ) -> Pin>>> { let props = ctx.props(); let base_url = props.base_url.clone(); @@ -151,7 +158,7 @@ impl LoadableComponent for ProxmoxPermissionPanel { }) } - fn main_view(&self, _ctx: &crate::LoadableComponentContext) -> Html { + fn main_view(&self, _ctx: &Context>) -> Html { DataTable::new(Rc::clone(&self.columns), self.store.clone()) .class("pwt-flex-fit") .into() diff --git a/src/subscription_panel.rs b/src/subscription_panel.rs index 9f8e65e..1b6a8e4 100644 --- a/src/subscription_panel.rs +++ b/src/subscription_panel.rs @@ -12,8 +12,13 @@ use pwt::widget::form::{Field, FormContext}; use pwt::widget::{Button, Container, InputPanel, Toolbar}; use crate::utils::render_epoch; -use crate::{ConfirmButton, DataViewWindow, EditWindow, KVGrid, KVGridRow, ProjectInfo}; -use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster}; +use crate::{ + ConfirmButton, DataViewWindow, EditWindow, KVGrid, KVGridRow, LoadableComponentState, + ProjectInfo, +}; +use crate::{ + LoadableComponent, LoadableComponentContext, LoadableComponentMaster, LoadableComponentScopeExt, +}; #[derive(Properties, PartialEq, Clone)] pub struct SubscriptionPanel { @@ -38,20 +43,26 @@ pub enum ViewState { SystemReport, } -pub enum Msg {} - pub struct ProxmoxSubscriptionPanel { + state: LoadableComponentState, rows: Rc>, data: Rc>>, } +crate::impl_deref_mut_property!( + ProxmoxSubscriptionPanel, + state, + LoadableComponentState +); + impl LoadableComponent for ProxmoxSubscriptionPanel { - type Message = Msg; + type Message = (); type Properties = SubscriptionPanel; type ViewState = ViewState; fn create(_ctx: &LoadableComponentContext) -> Self { Self { + state: LoadableComponentState::new(), rows: Rc::new(rows()), data: Rc::new(RefCell::new(Rc::new(Value::Null))), } @@ -59,7 +70,7 @@ impl LoadableComponent for ProxmoxSubscriptionPanel { fn load( &self, - ctx: &crate::LoadableComponentContext, + ctx: &Context>, ) -> Pin>>> { let data = self.data.clone(); let base_url = ctx.props().base_url.to_string(); @@ -70,7 +81,7 @@ impl LoadableComponent for ProxmoxSubscriptionPanel { }) } - fn toolbar(&self, ctx: &LoadableComponentContext) -> Option { + fn toolbar(&self, ctx: &Context>) -> Option { let toolbar = Toolbar::new() .class("pwt-overflow-hidden") .with_child( @@ -85,7 +96,7 @@ impl LoadableComponent for ProxmoxSubscriptionPanel { Button::new(tr!("Check")) .icon_class("fa fa-check-square-o") .onclick({ - let link = ctx.link(); + let link = ctx.link().clone(); let base_url = ctx.props().base_url.to_string(); move |_| { link.spawn({ @@ -112,7 +123,7 @@ impl LoadableComponent for ProxmoxSubscriptionPanel { html! {tr!("Are you sure you want to remove the subscription key?")}, ) .on_activate({ - let link = ctx.link(); + let link = ctx.link().clone(); let base_url = ctx.props().base_url.to_string(); move |_| { link.spawn({ @@ -141,8 +152,8 @@ impl LoadableComponent for ProxmoxSubscriptionPanel { ) .with_flex_spacer() .with_child({ - let loading = ctx.loading(); - let link = ctx.link(); + let loading = self.loading(); + let link = ctx.link().clone(); Button::refresh(loading).onclick(move |_| link.send_reload()) }); diff --git a/src/tasks.rs b/src/tasks.rs index d3e814f..6e1118e 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -26,7 +26,10 @@ use pbs_api_types::TaskListItem; use pwt_macros::builder; -use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster, TaskViewer}; +use crate::{ + LoadableComponent, LoadableComponentContext, LoadableComponentMaster, + LoadableComponentScopeExt, LoadableComponentState, TaskViewer, +}; use super::{TaskStatusSelector, TaskTypeSelector}; @@ -98,7 +101,6 @@ pub enum ViewDialog { } pub enum Msg { - Redraw, ToggleFilter, LoadBatch(bool), // fresh load LoadFinished, @@ -106,6 +108,7 @@ pub enum Msg { ShowTask, } pub struct ProxmoxTasks { + state: LoadableComponentState, selection: Selection, store: Store, show_filter: PersistentState, @@ -117,6 +120,8 @@ pub struct ProxmoxTasks { columns: Rc>>, } +crate::impl_deref_mut_property!(ProxmoxTasks, state, LoadableComponentState); + impl ProxmoxTasks { fn columns(ctx: &LoadableComponentContext) -> Rc>> { if let Some(columns) = ctx.props().columns.clone() { @@ -169,18 +174,21 @@ impl LoadableComponent for ProxmoxTasks { fn create(ctx: &LoadableComponentContext) -> Self { let link = ctx.link(); - let selection = Selection::new().on_select(link.callback(|_| Msg::Redraw)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); let store = Store::with_extract_key(|item: &TaskListItem| Key::from(item.upid.clone())); let filter_form_context = - FormContext::new().on_change(ctx.link().callback(|_| Msg::UpdateFilter)); + FormContext::new().on_change(ctx.link().custom_callback(|_| Msg::UpdateFilter)); let row_render_callback = DataTableRowRenderCallback::new({ let store = store.clone(); let link = link.clone(); move |args: &mut _| { if args.row_index() > store.data_len().saturating_sub(LOAD_BUFFER_ROWS) { - link.send_message(Msg::LoadBatch(false)); + link.send_custom_message(Msg::LoadBatch(false)); } let record: &TaskListItem = args.record(); match record.status.as_deref() { @@ -194,6 +202,7 @@ impl LoadableComponent for ProxmoxTasks { }); Self { + state: LoadableComponentState::new(), selection, store, show_filter: PersistentState::new("ProxmoxTasksShowFilter"), @@ -286,14 +295,13 @@ impl LoadableComponent for ProxmoxTasks { } store.append(&mut data); } - link.send_message(Msg::LoadFinished); + link.send_custom_message(Msg::LoadFinished); Ok(()) }) } fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { match msg { - Msg::Redraw => true, Msg::ToggleFilter => { self.show_filter.update(!*self.show_filter); true @@ -304,7 +312,7 @@ impl LoadableComponent for ProxmoxTasks { return false; } let filter_params = form_context.get_submit_data(); - if ctx.loading() && self.last_filter == filter_params { + if self.loading() && self.last_filter == filter_params { return false; } @@ -369,7 +377,7 @@ impl LoadableComponent for ProxmoxTasks { .with_child( Button::new(tr!("View")) .disabled(disabled) - .onclick(ctx.link().callback(|_| Msg::ShowTask)), + .onclick(ctx.link().custom_callback(|_| Msg::ShowTask)), ) .with_flex_spacer() .with_child({ @@ -381,12 +389,13 @@ impl LoadableComponent for ProxmoxTasks { .with_child( Button::new("Filter") .icon_class(filter_icon_class) - .onclick(ctx.link().callback(|_| Msg::ToggleFilter)), + .onclick(ctx.link().custom_callback(|_| Msg::ToggleFilter)), ) .with_child({ - let loading = ctx.loading(); - let link = ctx.link(); - Button::refresh(loading).onclick(move |_| link.send_message(Msg::LoadBatch(true))) + let loading = self.loading(); + let link = ctx.link().clone(); + Button::refresh(loading) + .onclick(move |_| link.send_custom_message(Msg::LoadBatch(true))) }); let filter_classes = classes!( @@ -441,13 +450,13 @@ impl LoadableComponent for ProxmoxTasks { fn main_view(&self, ctx: &LoadableComponentContext) -> Html { let columns = self.columns.clone(); - let link = ctx.link(); + let link = ctx.link().clone(); DataTable::new(columns, self.store.clone()) .class("pwt-flex-fit") .selection(self.selection.clone()) .on_row_dblclick(move |_: &mut _| { - link.send_message(Msg::ShowTask); + link.send_custom_message(Msg::ShowTask); }) .row_render_callback(self.row_render_callback.clone()) .into() diff --git a/src/tfa/tfa_view.rs b/src/tfa/tfa_view.rs index c4c1a43..540c98b 100644 --- a/src/tfa/tfa_view.rs +++ b/src/tfa/tfa_view.rs @@ -16,7 +16,10 @@ use pwt::widget::{Button, Mask, Toolbar}; use pwt_macros::builder; -use crate::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster}; +use crate::{ + impl_deref_mut_property, LoadableComponent, LoadableComponentContext, LoadableComponentMaster, + LoadableComponentScopeExt, LoadableComponentState, +}; use proxmox_tfa::{TfaType, TfaUser}; @@ -87,7 +90,6 @@ impl TfaView { } pub enum Msg { - Redraw, Edit, Remove(Option), RemoveResult(Result<(), Error>), @@ -104,11 +106,14 @@ pub enum ViewState { #[doc(hidden)] pub struct ProxmoxTfaView { + state: LoadableComponentState, selection: Selection, store: Store, removing: bool, } +impl_deref_mut_property!(ProxmoxTfaView, state, LoadableComponentState); + impl ProxmoxTfaView { fn get_selected_record(&self) -> Option { let selected_key = self.selection.selected_key(); @@ -127,8 +132,12 @@ impl LoadableComponent for ProxmoxTfaView { fn create(ctx: &LoadableComponentContext) -> Self { let store = Store::new(); - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::Redraw)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); Self { + state: LoadableComponentState::new(), store, selection, removing: false, @@ -177,7 +186,6 @@ impl LoadableComponent for ProxmoxTfaView { fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { let props = ctx.props(); match msg { - Msg::Redraw => true, Msg::Edit => { let info = match self.get_selected_record() { Some(info) => info, @@ -204,8 +212,8 @@ impl LoadableComponent for ProxmoxTfaView { // fixme: ask use if he really wants to remove let link = ctx.link().clone(); let base_url = props.base_url.clone(); - link.send_future(async move { - Msg::RemoveResult( + self.spawn(async move { + link.send_custom_message(Msg::RemoveResult( delete_item( base_url, info.user_id.clone(), @@ -213,7 +221,7 @@ impl LoadableComponent for ProxmoxTfaView { password, ) .await, - ) + )) }); false @@ -273,7 +281,7 @@ impl LoadableComponent for ProxmoxTfaView { .with_child( Button::new(tr!("Edit")) .disabled(edit_disabled) - .onclick(ctx.link().callback(|_| Msg::Edit)), + .onclick(ctx.link().custom_callback(|_| Msg::Edit)), ) .with_child( Button::new(tr!("Remove")) @@ -282,8 +290,8 @@ impl LoadableComponent for ProxmoxTfaView { ) .with_flex_spacer() .with_child({ - let loading = ctx.loading(); - let link = ctx.link(); + let loading = self.loading(); + let link = ctx.link().clone(); Button::refresh(loading).onclick(move |_| link.send_reload()) }); @@ -296,8 +304,8 @@ impl LoadableComponent for ProxmoxTfaView { .selection(self.selection.clone()) .class("pwt-flex-fit") .on_row_dblclick({ - let link = ctx.link(); - move |_: &mut _| link.send_message(Msg::Edit) + let link = ctx.link().clone(); + move |_: &mut _| link.send_custom_message(Msg::Edit) }); Mask::new(view).visible(self.removing).into() } @@ -314,8 +322,8 @@ impl LoadableComponent for ProxmoxTfaView { TfaConfirmRemove::new(info) .on_close(ctx.link().change_view_callback(|_| None)) .on_confirm({ - let link = ctx.link(); - move |password| link.send_message(Msg::Remove(password)) + let link = ctx.link().clone(); + move |password| link.send_custom_message(Msg::Remove(password)) }) .into() }), diff --git a/src/token_panel.rs b/src/token_panel.rs index 031d54f..66c25db 100644 --- a/src/token_panel.rs +++ b/src/token_panel.rs @@ -25,7 +25,8 @@ use crate::utils::{ }; use crate::{ AuthidSelector, ConfirmButton, EditWindow, LoadableComponent, LoadableComponentContext, - LoadableComponentLink, LoadableComponentMaster, PermissionPanel, + LoadableComponentMaster, LoadableComponentScope, LoadableComponentScopeExt, + LoadableComponentState, PermissionPanel, }; async fn load_api_tokens() -> Result, Error> { @@ -37,7 +38,7 @@ async fn load_api_tokens() -> Result, Error> { async fn create_token( form_ctx: FormContext, - link: LoadableComponentLink, + link: LoadableComponentScope, ) -> Result<(), Error> { let mut data = form_ctx.get_submit_data(); @@ -118,17 +119,19 @@ enum ViewState { } enum Msg { - Refresh, Remove, Regenerate, } struct ProxmoxTokenView { + state: LoadableComponentState, selection: Selection, store: Store, columns: Rc>>, } +crate::impl_deref_mut_property!(ProxmoxTokenView, state, LoadableComponentState); + fn token_api_url(user: &str, tokenname: &str) -> String { format!( "/access/users/{}/token/{}", @@ -146,11 +149,15 @@ impl LoadableComponent for ProxmoxTokenView { let link = ctx.link(); link.repeated_load(5000); - let selection = Selection::new().on_select(link.callback(|_| Msg::Refresh)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); let store = Store::with_extract_key(|record: &ApiToken| Key::from(record.tokenid.to_string())); Self { + state: LoadableComponentState::new(), selection, store, columns: columns(), @@ -193,7 +200,7 @@ impl LoadableComponent for ProxmoxTokenView { .confirm_message(tr!("Are you sure you want to remove the API token? \ All existing users of the token will lose access!")) .disabled(disabled) - .on_activate(link.callback(|_| Msg::Remove)), + .on_activate(link.custom_callback(|_| Msg::Remove)), ) .with_spacer() .with_child( @@ -203,7 +210,7 @@ impl LoadableComponent for ProxmoxTokenView { All existing users of the token will lose access!" )) .disabled(disabled) - .on_activate(link.callback(|_| Msg::Regenerate)), + .on_activate(link.custom_callback(|_| Msg::Regenerate)), ) .with_spacer() .with_child( @@ -217,7 +224,6 @@ impl LoadableComponent for ProxmoxTokenView { fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { match msg { - Msg::Refresh => true, Msg::Remove => { let Some(record) = self.store.selected_record(&self.selection) else { return false; @@ -230,7 +236,7 @@ impl LoadableComponent for ProxmoxTokenView { }; let url = token_api_url(&user, tokenname.as_str()); - let link = ctx.link(); + let link = ctx.link().clone(); link.clone().spawn(async move { match crate::http_delete(url, None).await { Ok(()) => { @@ -271,7 +277,7 @@ impl LoadableComponent for ProxmoxTokenView { } fn main_view(&self, ctx: &LoadableComponentContext) -> Html { - let link = ctx.link(); + let link = ctx.link().clone(); DataTable::new(self.columns.clone(), self.store.clone()) .class("pwt-flex-fit") diff --git a/src/user_panel.rs b/src/user_panel.rs index d364df8..cbc6b49 100644 --- a/src/user_panel.rs +++ b/src/user_panel.rs @@ -23,8 +23,9 @@ use crate::form::delete_empty_values; use crate::percent_encoding::percent_encode_component; use crate::utils::{epoch_to_input_value, render_epoch_short}; use crate::{ - EditWindow, LoadableComponent, LoadableComponentContext, LoadableComponentMaster, - PermissionPanel, RealmSelector, SchemaValidation, + impl_deref_mut_property, EditWindow, LoadableComponent, LoadableComponentContext, + LoadableComponentMaster, LoadableComponentScopeExt, LoadableComponentState, PermissionPanel, + RealmSelector, SchemaValidation, }; async fn load_user_list() -> Result, Error> { @@ -111,15 +112,17 @@ pub enum ViewState { } pub enum Msg { - SelectionChange, RemoveItem, } pub struct ProxmoxUserPanel { + state: LoadableComponentState, store: Store, selection: Selection, } +impl_deref_mut_property!(ProxmoxUserPanel, state, LoadableComponentState); + impl LoadableComponent for ProxmoxUserPanel { type Message = Msg; type Properties = UserPanel; @@ -142,17 +145,23 @@ impl LoadableComponent for ProxmoxUserPanel { Key::from(record.user.userid.as_str()) }); - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::SelectionChange)); + let selection = Selection::new().on_select({ + let link = ctx.link().clone(); + move |_| link.send_redraw() + }); - Self { store, selection } + Self { + state: LoadableComponentState::new(), + store, + selection, + } } fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { match msg { - Msg::SelectionChange => true, Msg::RemoveItem => { if let Some(key) = self.selection.selected_key() { - let link = ctx.link(); + let link = ctx.link().clone(); link.clone().spawn(async move { if let Err(err) = delete_user(key).await { link.show_error(tr!("Unable to delete user"), err, true); @@ -191,7 +200,7 @@ impl LoadableComponent for ProxmoxUserPanel { .with_child( Button::new(tr!("Remove")) .disabled(no_selection) - .onclick(link.callback(|_| Msg::RemoveItem)), + .onclick(link.custom_callback(|_| Msg::RemoveItem)), ) .with_spacer() .with_child( @@ -206,8 +215,8 @@ impl LoadableComponent for ProxmoxUserPanel { ) .with_flex_spacer() .with_child({ - let loading = ctx.loading(); - let link = ctx.link(); + let loading = self.loading(); + let link = ctx.link().clone(); Button::refresh(loading).onclick(move |_| link.send_reload()) }); @@ -215,7 +224,7 @@ impl LoadableComponent for ProxmoxUserPanel { } fn main_view(&self, ctx: &LoadableComponentContext) -> Html { - let link = ctx.link(); + let link = ctx.link().clone(); DataTable::new(columns(), self.store.clone()) .class("pwt-flex-fill pwt-overflow-auto") .selection(self.selection.clone()) -- 2.47.3 _______________________________________________ yew-devel mailing list yew-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel