* [yew-devel] [RFC yew-comp V2] refactor: move LoadableComponent state into component implementations
@ 2025-12-10 9:28 Dietmar Maurer
2025-12-10 11:27 ` [yew-devel] applied: " Thomas Lamprecht
0 siblings, 1 reply; 2+ messages in thread
From: Dietmar Maurer @ 2025-12-10 9:28 UTC (permalink / raw)
To: 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.
- avoid useless Redraw/Datachange/Refresh messages, because `LoadableComponentMaster`
already implements that.
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
Changes in V2:
- fix typos found by Thomas
- avoid send_custom_message() and custom_callback()
src/acl/acl_view.rs | 24 +-
src/acme/acme_accounts.rs | 28 +-
src/acme/acme_domains.rs | 31 +-
src/acme/acme_plugins.rs | 40 +-
src/acme/certificate_list.rs | 36 +-
src/apt_package_manager.rs | 43 +-
src/apt_repositories.rs | 40 +-
src/auth_view.rs | 28 +-
src/configuration/network_view.rs | 32 +-
src/configuration/pve/lxc_network_panel.rs | 26 +-
src/lib.rs | 33 +-
src/loadable_component.rs | 689 +++++++++++----------
src/node_status_panel.rs | 27 +-
src/notes_view.rs | 14 +-
src/object_grid.rs | 26 +-
src/permission_panel.rs | 17 +-
src/subscription_panel.rs | 33 +-
src/tasks.rs | 24 +-
src/tfa/tfa_view.rs | 30 +-
src/token_panel.rs | 20 +-
src/user_panel.rs | 29 +-
21 files changed, 760 insertions(+), 510 deletions(-)
diff --git a/src/acl/acl_view.rs b/src/acl/acl_view.rs
index 58da3fd..ee197b0 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<ViewState>,
selection: Selection,
store: Store<AclListItem>,
}
+impl_deref_mut_property!(ProxmoxAclView, state, LoadableComponentState<ViewState>);
+
impl ProxmoxAclView {
fn colmuns() -> Rc<Vec<DataTableHeader<AclListItem>>> {
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(
@@ -211,14 +222,13 @@ impl LoadableComponent for ProxmoxAclView {
fn update(&mut self, ctx: &LoadableComponentContext<Self>, 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<ViewState>,
selection: Selection,
store: Store<AcmeAccountEntry>,
columns: Rc<Vec<DataTableHeader<AcmeAccountEntry>>>,
}
-pub enum Msg {
- Redraw,
-}
+impl_deref_mut_property!(
+ ProxmoxAcmeAccountsPanel,
+ state,
+ LoadableComponentState<ViewState>
+);
#[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>) -> 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..2d1ec5c 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<ViewState>,
selection: Selection,
store: Store<AcmeDomainEntry>,
columns: Rc<Vec<DataTableHeader<AcmeDomainEntry>>>,
acme_account: Option<AcmeConfig>,
}
+impl_deref_mut_property!(
+ ProxmoxAcmeDomainsPanel,
+ state,
+ LoadableComponentState<ViewState>
+);
+
pub enum Msg {
- Redraw,
AcmeAccount(Option<AcmeConfig>),
}
@@ -78,7 +86,10 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel {
type ViewState = ViewState;
fn create(ctx: &LoadableComponentContext<Self>) -> 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<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
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?;
@@ -157,7 +169,6 @@ impl LoadableComponent for ProxmoxAcmeDomainsPanel {
fn update(&mut self, _ctx: &LoadableComponentContext<Self>, 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 {
</div>}
})
.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..f5545b9 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<ViewState>,
selection: Selection,
store: Store<PluginConfig>,
columns: Rc<Vec<DataTableHeader<PluginConfig>>>,
challenge_schema: Option<AcmeChallengeSchemaItem>,
schema_info: ChallengeSchemaInfo,
- async_pool: AsyncPool,
}
+impl_deref_mut_property!(
+ ProxmoxAcmePluginsPanel,
+ state,
+ LoadableComponentState<ViewState>
+);
+
#[derive(PartialEq)]
pub enum ViewState {
Add,
@@ -96,7 +103,6 @@ pub enum ViewState {
}
pub enum Msg {
- Redraw,
CloseDialog,
Add,
Edit(Key),
@@ -126,7 +132,10 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel {
type ViewState = ViewState;
fn create(ctx: &LoadableComponentContext<Self>) -> 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()));
@@ -136,6 +145,7 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel {
ctx.link().send_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<Self>, msg: Self::Message) -> bool {
match msg {
- Msg::Redraw => true,
Msg::Add => {
self.challenge_schema = None;
ctx.link().change_view(Some(ViewState::Add));
@@ -217,8 +225,8 @@ 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));
});
@@ -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();
@@ -270,7 +278,7 @@ impl LoadableComponent for ProxmoxAcmePluginsPanel {
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 {
@@ -297,7 +305,7 @@ 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() {
@@ -372,7 +380,7 @@ impl ProxmoxAcmePluginsPanel {
}
fn dns_plugin_input_panel(
- link: &LoadableComponentLink<Self>,
+ link: &LoadableComponentScope<Self>,
form_ctx: &FormContext,
id: Option<&str>,
challenge_schema: Option<&AcmeChallengeSchemaItem>,
@@ -498,7 +506,7 @@ impl ProxmoxAcmePluginsPanel {
.on_done(ctx.link().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| {
@@ -534,7 +542,7 @@ impl ProxmoxAcmePluginsPanel {
EditWindow::new(tr!("Add") + ": " + &tr!("ACME DNS Plugin"))
.on_done(ctx.link().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<Value>),
@@ -59,24 +56,35 @@ pub enum ViewState {
#[doc(hidden)]
pub struct ProxmoxCertificateList {
+ state: LoadableComponentState<ViewState>,
selection: Selection,
store: Store<CertificateInfo>,
columns: Rc<Vec<DataTableHeader<CertificateInfo>>>,
rows: Rc<Vec<KVGridRow>>,
}
+impl_deref_mut_property!(
+ ProxmoxCertificateList,
+ state,
+ LoadableComponentState<ViewState>
+);
+
impl LoadableComponent for ProxmoxCertificateList {
type Properties = CertificateList;
- type Message = Msg;
+ type Message = ();
type ViewState = ViewState;
fn create(ctx: &LoadableComponentContext<Self>) -> 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<Self>) -> 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..7c77aa4 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<ViewState>,
tree_store: TreeStore<TreeEntry>,
selection: Selection,
columns: Rc<Vec<DataTableHeader<TreeEntry>>>,
- async_pool: AsyncPool,
}
+crate::impl_deref_mut_property!(
+ ProxmoxAptPackageManager,
+ state,
+ LoadableComponentState<ViewState>
+);
+
impl LoadableComponent for ProxmoxAptPackageManager {
type Properties = AptPackageManager;
type Message = Msg;
type ViewState = ViewState;
fn create(ctx: &LoadableComponentContext<Self>) -> 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::<Value>(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,9 +274,8 @@ 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);
@@ -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..5e8eee6 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<TreeEnt
}
pub enum Msg {
- Refresh,
ToggleEnable,
UpdateStatus(APTRepositoriesResult),
SubscriptionInfo(Result<Value, Error>),
@@ -419,6 +419,7 @@ pub enum ViewState {
}
pub struct ProxmoxAptRepositories {
+ state: LoadableComponentState<ViewState>,
tree_store: TreeStore<TreeEntry>,
selection: Selection,
columns: Rc<Vec<DataTableHeader<TreeEntry>>>,
@@ -430,6 +431,12 @@ pub struct ProxmoxAptRepositories {
status_columns: Rc<Vec<DataTableHeader<StatusLine>>>,
}
+crate::impl_deref_mut_property!(
+ ProxmoxAptRepositories,
+ state,
+ LoadableComponentState<ViewState>
+);
+
impl LoadableComponent for ProxmoxAptRepositories {
type Properties = AptRepositories;
type Message = Msg;
@@ -438,19 +445,27 @@ impl LoadableComponent for ProxmoxAptRepositories {
fn create(ctx: &LoadableComponentContext<Self>) -> 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_message(Msg::SubscriptionInfo(data));
+ }
});
Self {
+ state,
tree_store,
selection,
columns,
@@ -470,7 +485,7 @@ 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?;
@@ -484,7 +499,6 @@ impl LoadableComponent for ProxmoxAptRepositories {
fn update(&mut self, ctx: &LoadableComponentContext<Self>, 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(()) => {
@@ -610,8 +624,8 @@ impl LoadableComponent for ProxmoxAptRepositories {
})
.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..82f6041 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<ViewState>,
selection: Selection,
store: Store<BasicRealmInfo>,
}
+crate::impl_deref_mut_property!(ProxmoxAuthView, state, LoadableComponentState<ViewState>);
+
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<ProxmoxAuthView>,
+ link: LoadableComponentScope<ProxmoxAuthView>,
url: impl Into<String>,
) -> Result<(), Error> {
let mut data = form_ctx.get_submit_data();
@@ -178,8 +182,15 @@ impl LoadableComponent for ProxmoxAuthView {
fn create(ctx: &LoadableComponentContext<Self>) -> 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);
@@ -341,7 +351,7 @@ impl LoadableComponent for ProxmoxAuthView {
.selection(self.selection.clone())
.class("pwt-flex-fit")
.on_row_dblclick({
- let link = ctx.link();
+ let link = ctx.link().clone();
move |_: &mut _| link.send_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..213de53 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<ViewState>,
columns: Rc<Vec<DataTableHeader<Interface>>>,
store: Store<Interface>,
changes: String,
selection: Selection,
}
+crate::impl_deref_mut_property!(ProxmoxNetworkView, state, LoadableComponentState<ViewState>);
+
#[derive(PartialEq)]
pub enum ViewState {
AddBridge,
@@ -81,7 +87,6 @@ pub enum ViewState {
}
pub enum Msg {
- SelectionChange,
RemoveItem,
Changes(String),
RevertChanges,
@@ -119,7 +124,7 @@ impl LoadableComponent for ProxmoxNetworkView {
ctx: &LoadableComponentContext<Self>,
) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
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);
@@ -130,8 +135,12 @@ impl LoadableComponent for ProxmoxNetworkView {
fn create(ctx: &LoadableComponentContext<Self>) -> 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<Self>, 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) => {
@@ -239,8 +247,8 @@ impl LoadableComponent for ProxmoxNetworkView {
)
.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<Self>) -> 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..807b145 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<ViewState>,
columns: Rc<Vec<DataTableHeader<NetworkEntry>>>,
store: Store<NetworkEntry>,
selection: Selection,
}
+impl_deref_mut_property!(LxcNetworkComp, state, LoadableComponentState<ViewState>);
+
impl LxcNetworkComp {
fn edit_dialog(&self, ctx: &LoadableComponentContext<Self>, name: Option<String>) -> Html {
let props = ctx.props();
- let link = ctx.link();
+ let link = ctx.link().clone();
let property = lxc_network_property(
Some(props.node.clone()),
@@ -157,11 +162,15 @@ impl LoadableComponent for LxcNetworkComp {
fn create(ctx: &LoadableComponentContext<Self>) -> Self {
let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::SelectionChange));
- let store = Store::new().on_change(ctx.link().callback(|_| Msg::Redraw));
+ 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 +179,14 @@ impl LoadableComponent for LxcNetworkComp {
fn update(&mut self, ctx: &LoadableComponentContext<Self>, 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 {
@@ -259,7 +267,7 @@ impl LoadableComponent for LxcNetworkComp {
fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> 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<LanguageInfo> {
),
]
}
+
+#[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..7680000 100644
--- a/src/loadable_component.rs
+++ b/src/loadable_component.rs
@@ -1,163 +1,296 @@
-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<String>,
- repeat_timespan: u32, /* 0 => no repeated loading */
- task_base_url: Option<AttrValue>,
-}
+pub type LoadableComponentContext<L> = Context<LoadableComponentMaster<L>>;
+pub type LoadableComponentScope<L> = Scope<LoadableComponentMaster<L>>;
+
+/// 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<Key>,
+/// /* 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 incluce a LoadableComponentState
+/// state: LoadableComponentState<ViewState>,
+/// // Add any other data you need
+/// loaded_data: Option<String>,
+/// }
+///
+/// // implement DerefMut
+/// proxmox_yew_comp::impl_deref_mut_property!(
+/// MyComponentState,
+/// state,
+/// LoadableComponentState<ViewState>
+/// );
+///
+/// impl LoadableComponent for MyComponentState {
+/// type Properties = MyComponent;
+/// type Message = Msg; // component message type
+/// type ViewState = ViewState;
+///
+/// fn create(ctx: &LoadableComponentContext<Self>) -> Self {
+/// Self {
+/// state: LoadableComponentState::new(),
+/// loaded_data: None,
+/// }
+/// }
+///
+/// fn load(
+/// &self,
+/// ctx: &LoadableComponentContext<Self>,
+/// ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
+/// 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<Self>, msg: Self::Message) -> bool {
+/// match msg {
+/// Msg::UpdateData(data) => self.loaded_data = Some(data),
+/// }
+/// true
+/// }
+///
+/// fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> 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<MyComponent> for VNode {
+/// fn from(props: MyComponent) -> VNode {
+/// let key = props.key.clone();
+/// let comp = VComp::new::<LoadableComponentMaster<MyComponentState>>(Rc::new(props), key);
+/// VNode::from(comp)
+/// }
+/// }
+///
+/// ```
+pub trait LoadableComponent:
+ Sized + DerefMut<Target = LoadableComponentState<Self::ViewState>> + '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<LoadableComponentMaster<L>>,
- comp_state: &'a LoadableComponentState,
-}
+ /// Create a new instance
+ fn create(ctx: &LoadableComponentContext<Self>) -> Self;
-impl<L: LoadableComponent + Sized> LoadableComponentContext<'_, L> {
- pub fn props(&self) -> &L::Properties {
- self.ctx.props()
- }
- pub fn link(&self) -> LoadableComponentLink<L> {
- LoadableComponentLink {
- link: self.ctx.link().clone(),
- }
- }
- pub fn loading(&self) -> bool {
- self.comp_state.loading > 0
- }
+ /// Async Load
+ fn load(
+ &self,
+ ctx: &LoadableComponentContext<Self>,
+ ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>>;
- pub fn last_load_errors(&self) -> Option<&str> {
- self.comp_state.last_load_error.as_deref()
+ /// Yew component update function (see [Component::update])
+ #[allow(unused_variables)]
+ fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+ true
}
-}
-pub struct LoadableComponentLink<L: LoadableComponent + Sized + 'static> {
- link: Scope<LoadableComponentMaster<L>>,
-}
-
-impl<L: LoadableComponent + Sized> Clone for LoadableComponentLink<L> {
- fn clone(&self) -> Self {
- Self {
- link: self.link.clone(),
- }
+ /// Yew component changed function (see [Component::changed])
+ #[allow(unused_variables)]
+ fn changed(
+ &mut self,
+ ctx: &LoadableComponentContext<Self>,
+ _old_props: &Self::Properties,
+ ) -> bool {
+ true
}
-}
-impl<L: LoadableComponent + Sized> LoadableComponentLink<L> {
- pub fn send_message(&self, msg: impl Into<L::Message>) {
- let msg = msg.into();
- self.link.send_message(Msg::ChildMessage(msg));
+ /// Optional toolbar
+ #[allow(unused_variables)]
+ fn toolbar(&self, ctx: &LoadableComponentContext<Self>) -> Option<Html> {
+ None
}
- pub fn callback<F, IN, M>(&self, function: F) -> Callback<IN>
- where
- M: Into<L::Message>,
- F: Fn(IN) -> M + 'static,
- {
- self.link.callback(move |p: IN| {
- let msg: L::Message = function(p).into();
- Msg::ChildMessage(msg)
- })
- }
+ /// 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<Self>) -> Html;
- /// Spawn a future using the [AsyncPool] from the component.
- pub fn spawn<Fut>(&self, future: Fut)
- where
- Fut: Future<Output = ()> + 'static,
- {
- self.link.send_message(Msg::Spawn(Box::pin(future)));
+ /// 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<Self>,
+ view_state: &Self::ViewState,
+ ) -> Option<Html> {
+ None
}
- pub fn send_future<Fut, M>(&self, future: Fut)
- where
- M: Into<L::Message>,
- Fut: Future<Output = M> + '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));
- })));
- }
+ /// Yew component rendered function (see [Component::rendered])
+ #[allow(unused_variables)]
+ fn rendered(&mut self, ctx: &LoadableComponentContext<Self>, first_render: bool) {}
+}
- pub fn callback_future<F, Fut, IN, M>(&self, function: F) -> Callback<IN>
- where
- M: Into<L::Message>,
- Fut: Future<Output = M> + 'static,
- F: Fn(IN) -> Fut + 'static,
- {
- let link = self.clone();
+#[derive(Clone, PartialEq)]
+pub enum ViewState<V: PartialEq> {
+ 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<i64>),
+ /// Show an error message dialog
+ Error(String, String, /* reload_on_close */ bool),
+}
- let closure = move |input: IN| {
- link.send_future(function(input));
- };
+pub enum Msg<M, V: PartialEq> {
+ DataChange,
+ Load,
+ RepeatedLoad(u32 /* repeat time in miliseconds */),
+ LoadResult(Result<(), Error>),
+ ChangeView(/*reload*/ bool, ViewState<V>),
+ ChildMessage(M),
+ Visible(bool),
+ Spawn(Pin<Box<dyn Future<Output = ()>>>),
+}
- closure.into()
+impl<M, V: PartialEq> From<M> for Msg<M, V> {
+ fn from(value: M) -> Self {
+ Msg::ChildMessage(value)
}
+}
- pub fn send_reload(&self) {
- self.link.send_message(Msg::Load)
- }
+pub trait LoadableComponentScopeExt<M, V: PartialEq> {
+ fn send_reload(&self);
+ fn send_redraw(&self);
+ fn repeated_load(&self, miliseconds: u32);
- pub fn repeated_load(&self, miliseconds: u32) {
- self.link.send_message(Msg::RepeatedLoad(miliseconds));
- }
+ fn change_view(&self, child_view_state: Option<V>);
+ fn change_view_callback<C, F, IN>(&self, function: F) -> Callback<IN>
+ where
+ C: Into<Option<V>>,
+ F: Fn(IN) -> C + 'static;
- pub fn task_base_url(&self, base_url: impl Into<AttrValue>) {
- self.link.send_message(Msg::TaskBaseUrl(base_url.into()));
- }
+ /// Spawn a future using the [AsyncPool] from the component.
+ fn spawn<Fut>(&self, future: Fut)
+ where
+ Fut: Future<Output = ()> + 'static;
- pub fn show_error(
+ fn show_error(
&self,
title: impl Into<String>,
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 show_task_progres(&self, task_id: impl Into<String>);
+
+ fn show_task_log(&self, task_id: impl Into<String>, endtime: Option<i64>);
+
+ fn start_task(&self, command_path: impl Into<String>, data: Option<Value>, short: bool);
+}
+
+impl<M, V: PartialEq, T: 'static + LoadableComponent<Message = M, ViewState = V>>
+ LoadableComponentScopeExt<M, V> for Scope<LoadableComponentMaster<T>>
+{
+ fn send_reload(&self) {
+ self.send_message(Msg::Load);
}
- pub fn show_task_progres(&self, task_id: impl Into<String>) {
- let view_state = ViewState::TaskProgress(task_id.into());
- self.link.send_message(Msg::ChangeView(false, view_state));
+ fn send_redraw(&self) {
+ self.send_message(Msg::DataChange);
}
- pub fn show_task_log(&self, task_id: impl Into<String>, endtime: Option<i64>) {
- let view_state = ViewState::TaskLog(task_id.into(), endtime);
- self.link.send_message(Msg::ChangeView(false, view_state));
+ fn repeated_load(&self, miliseconds: u32) {
+ self.send_message(Msg::RepeatedLoad(miliseconds));
}
- pub fn change_view(&self, child_view_state: Option<L::ViewState>) {
+ fn change_view(&self, child_view_state: Option<V>) {
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<F, IN, M>(&self, function: F) -> Callback<IN>
+ fn change_view_callback<C, F, IN>(&self, function: F) -> Callback<IN>
where
- M: Into<Option<L::ViewState>>,
- F: Fn(IN) -> M + 'static,
+ C: Into<Option<V>>,
+ F: Fn(IN) -> C + 'static,
{
- self.link.callback(move |p: IN| {
- let state: Option<L::ViewState> = function(p).into();
+ self.callback(move |p: IN| {
+ let state: Option<V> = function(p).into();
if let Some(state) = state {
Msg::ChangeView(true, ViewState::Dialog(state))
} else {
@@ -166,11 +299,38 @@ impl<L: LoadableComponent + Sized> LoadableComponentLink<L> {
})
}
- pub fn start_task(&self, command_path: impl Into<String>, data: Option<Value>, short: bool) {
+ fn spawn<Fut>(&self, future: Fut)
+ where
+ Fut: Future<Output = ()> + 'static,
+ {
+ self.send_message(Msg::Spawn(Box::pin(future)));
+ }
+
+ fn show_error(
+ &self,
+ title: impl Into<String>,
+ 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<String>) {
+ 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<String>, endtime: Option<i64>) {
+ 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<String>, data: Option<Value>, short: bool) {
let command_path: String = command_path.into();
let link = self.clone();
let command_future = crate::http_post::<String>(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 +347,86 @@ impl<L: LoadableComponent + Sized> LoadableComponentLink<L> {
}
})));
}
-
- /// 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<LoadableComponentMaster<L>> {
- &self.link
- }
}
-impl<L: LoadableComponent + Sized> RouterScopeExt for LoadableComponentLink<L> {
- fn navigator(&self) -> Option<yew_router::prelude::Navigator> {
- self.link.navigator()
- }
-
- fn location(&self) -> Option<yew_router::prelude::Location> {
- self.link.location()
- }
-
- fn route<R>(&self) -> Option<R>
- where
- R: yew_router::Routable + 'static,
- {
- self.link.route()
- }
-
- fn add_location_listener(
- &self,
- cb: Callback<yew_router::prelude::Location>,
- ) -> Option<yew_router::prelude::LocationHandle> {
- self.link.add_location_listener(cb)
- }
-
- fn add_navigator_listener(
- &self,
- cb: Callback<yew_router::prelude::Navigator>,
- ) -> Option<yew_router::prelude::NavigatorHandle> {
- 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<ViewState>,
+/// // Add any other data you need
+/// other_data: String,
+/// }
+/// // implement DerefMut
+/// proxmox_yew_comp::impl_deref_mut_property!(MyComponentState, state, LoadableComponentState<ViewState>);
+/// ```
+pub struct LoadableComponentState<V: PartialEq> {
+ loading: usize,
+ last_load_error: Option<String>,
+ repeat_timespan: u32, /* 0 => no repeated loading */
+ task_base_url: Option<AttrValue>,
+ view_state: ViewState<V>,
+ reload_timeout: Option<Timeout>,
+ visible: bool,
+ visibitlity_observer: Option<DomVisibilityObserver>,
+ node_ref: NodeRef,
+ async_pool: AsyncPool,
}
-impl<L: LoadableComponent + Sized> NavigationContextExt for LoadableComponentLink<L> {
- fn nav_context(&self) -> Option<pwt::state::NavigationContext> {
- self.link.nav_context()
- }
-
- fn full_path(&self) -> Option<String> {
- self.link.full_path()
- }
-
- fn push_relative_route(&self, path: &str) {
- self.link.push_relative_route(path)
+impl<V: PartialEq> LoadableComponentState<V> {
+ 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>) -> Self;
-
- fn load(
- &self,
- ctx: &LoadableComponentContext<Self>,
- ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>>;
-
- #[allow(unused_variables)]
- fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
- true
+ pub fn loading(&self) -> bool {
+ self.loading > 0
}
- #[allow(unused_variables)]
- fn changed(
- &mut self,
- ctx: &LoadableComponentContext<Self>,
- _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<Self>) -> Option<Html> {
- 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<Self>) -> Html;
-
- #[allow(unused_variables)]
- fn dialog_view(
- &self,
- ctx: &LoadableComponentContext<Self>,
- view_state: &Self::ViewState,
- ) -> Option<Html> {
- None
+ /// Spawn a future using the [AsyncPool] from the component.
+ pub fn spawn<Fut>(&self, future: Fut)
+ where
+ Fut: Future<Output = ()> + 'static,
+ {
+ self.async_pool.spawn(future);
}
-
- #[allow(unused_variables)]
- fn rendered(&mut self, ctx: &LoadableComponentContext<Self>, first_render: bool) {}
-}
-
-#[derive(Clone, PartialEq)]
-pub enum ViewState<V: PartialEq> {
- 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<i64>),
- /// Show an error message dialog
- Error(String, String, /* reload_on_close */ bool),
-}
-
-pub enum Msg<M, V: PartialEq> {
- DataChange,
- Load,
- RepeatedLoad(u32 /* repeat time in miliseconds */),
- LoadResult(Result<(), Error>),
- ChangeView(/*reload*/ bool, ViewState<V>),
- ChildMessage(M),
- TaskBaseUrl(AttrValue),
- Visible(bool),
- Spawn(Pin<Box<dyn Future<Output = ()>>>),
}
+#[doc(hidden)]
pub struct LoadableComponentMaster<L: LoadableComponent> {
state: L,
- comp_state: LoadableComponentState,
- view_state: ViewState<L::ViewState>,
- reload_timeout: Option<Timeout>,
- visible: bool,
- visibitlity_observer: Option<DomVisibilityObserver>,
- node_ref: NodeRef,
- async_pool: AsyncPool,
}
impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
@@ -328,91 +434,65 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
type Properties = L::Properties;
fn create(ctx: &Context<Self>) -> 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<Self>, 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 {
<Self as yew::Component>::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 +501,7 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
true
}
Msg::ChangeView(reload_data, view_state) => {
- if self.view_state == view_state {
+ if self.state.view_state == view_state {
return false;
}
@@ -429,29 +509,21 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
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 {
<Self as yew::Component>::update(self, ctx, Msg::Load);
}
}
@@ -461,26 +533,16 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
}
fn changed(&mut self, ctx: &Context<Self>, _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<Self>) -> 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<Html> =
- 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 +560,7 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
.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 +572,19 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
.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 +595,18 @@ impl<L: LoadableComponent + 'static> Component for LoadableComponentMaster<L> {
.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<Self>, first_render: bool) {
- if self.visibitlity_observer.is_none() && self.reload_timeout.is_some() {
- if let Some(el) = self.node_ref.cast::<web_sys::Element>() {
- 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::<web_sys::Element>() {
+ 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..f7d4810 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<NodeStatus>),
RebootOrShutdown(NodePowerCommand),
- Reload,
}
#[derive(PartialEq)]
@@ -62,10 +61,17 @@ enum ViewState {
}
struct ProxmoxNodeStatusPanel {
+ state: LoadableComponentState<ViewState>,
node_status: Option<Rc<NodeStatus>>,
error: Option<Error>,
}
+crate::impl_deref_mut_property!(
+ ProxmoxNodeStatusPanel,
+ state,
+ LoadableComponentState<ViewState>
+);
+
impl ProxmoxNodeStatusPanel {
fn change_power_state(&self, ctx: &LoadableComponentContext<Self>, command: NodePowerCommand) {
let Some(url) = ctx.props().status_base_url.clone() else {
@@ -79,7 +85,7 @@ impl ProxmoxNodeStatusPanel {
}));
match http_post(url.as_str(), data).await {
- Ok(()) => link.send_message(Msg::Reload),
+ Ok(()) => link.send_redraw(),
Err(err) => link.send_message(Msg::Error(err)),
}
});
@@ -90,8 +96,8 @@ impl ProxmoxNodeStatusPanel {
ctx: &LoadableComponentContext<Self>,
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>) -> Self {
+ fn create(ctx: &LoadableComponentContext<Self>) -> 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<Self>,
+ ctx: &LoadableComponentContext<Self>,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
let url = ctx.props().status_base_url.clone();
let link = ctx.link().clone();
@@ -162,7 +168,7 @@ impl LoadableComponent for ProxmoxNodeStatusPanel {
})
}
- fn update(&mut self, ctx: &crate::LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+ fn update(&mut self, ctx: &LoadableComponentContext<Self>, 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<Self>) -> Html {
+ fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
let status = self
.node_status
.as_ref()
diff --git a/src/notes_view.rs b/src/notes_view.rs
index 6b0a379..5a96408 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<ViewState>,
data: NotesWithDigest,
edit_window_loader: ApiLoadCallback<Value>,
}
+crate::impl_deref_mut_property!(ProxmoxNotesView, state, LoadableComponentState<ViewState>);
+
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,7 +147,7 @@ impl LoadableComponent for ProxmoxNotesView {
ctx: &LoadableComponentContext<Self>,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
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;
@@ -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..40214a3 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<ViewState>,
+
selection: Option<Key>,
data: Rc<Value>,
@@ -266,6 +271,8 @@ pub struct PwtObjectGrid {
controller_observer: Option<SharedStateObserver<Vec<ObjectGridCommand>>>,
}
+crate::impl_deref_mut_property!(PwtObjectGrid, state, LoadableComponentState<ViewState>);
+
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<Self>) {
+ fn update_controller(
+ &mut self,
+ props: &ObjectGrid,
+ link: Scope<LoadableComponentMaster<Self>>,
+ ) {
match &props.controller {
None => self.controller_observer = None,
Some(controller) => {
@@ -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,7 +364,7 @@ impl LoadableComponent for PwtObjectGrid {
) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
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 {
@@ -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));
}
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<PermissionInfo>,
columns: Rc<Vec<DataTableHeader<PermissionInfo>>>,
}
+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>) -> Self {
+ fn create(_ctx: &Context<LoadableComponentMaster<Self>>) -> 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<Self>,
+ ctx: &Context<LoadableComponentMaster<Self>>,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
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<Self>) -> Html {
+ fn main_view(&self, _ctx: &Context<LoadableComponentMaster<Self>>) -> 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<ViewState>,
rows: Rc<Vec<KVGridRow>>,
data: Rc<RefCell<Rc<Value>>>,
}
+crate::impl_deref_mut_property!(
+ ProxmoxSubscriptionPanel,
+ state,
+ LoadableComponentState<ViewState>
+);
+
impl LoadableComponent for ProxmoxSubscriptionPanel {
- type Message = Msg;
+ type Message = ();
type Properties = SubscriptionPanel;
type ViewState = ViewState;
fn create(_ctx: &LoadableComponentContext<Self>) -> 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<Self>,
+ ctx: &Context<LoadableComponentMaster<Self>>,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
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<Self>) -> Option<Html> {
+ fn toolbar(&self, ctx: &Context<LoadableComponentMaster<Self>>) -> Option<Html> {
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..8dec24f 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<ViewDialog>,
selection: Selection,
store: Store<TaskListItem>,
show_filter: PersistentState<bool>,
@@ -117,6 +120,8 @@ pub struct ProxmoxTasks {
columns: Rc<Vec<DataTableHeader<TaskListItem>>>,
}
+crate::impl_deref_mut_property!(ProxmoxTasks, state, LoadableComponentState<ViewDialog>);
+
impl ProxmoxTasks {
fn columns(ctx: &LoadableComponentContext<Self>) -> Rc<Vec<DataTableHeader<TaskListItem>>> {
if let Some(columns) = ctx.props().columns.clone() {
@@ -169,7 +174,10 @@ impl LoadableComponent for ProxmoxTasks {
fn create(ctx: &LoadableComponentContext<Self>) -> 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 =
@@ -194,6 +202,7 @@ impl LoadableComponent for ProxmoxTasks {
});
Self {
+ state: LoadableComponentState::new(),
selection,
store,
show_filter: PersistentState::new("ProxmoxTasksShowFilter"),
@@ -293,7 +302,6 @@ impl LoadableComponent for ProxmoxTasks {
fn update(&mut self, ctx: &LoadableComponentContext<Self>, 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;
}
@@ -384,8 +392,8 @@ impl LoadableComponent for ProxmoxTasks {
.onclick(ctx.link().callback(|_| Msg::ToggleFilter)),
)
.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_message(Msg::LoadBatch(true)))
});
@@ -441,7 +449,7 @@ impl LoadableComponent for ProxmoxTasks {
fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> 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")
diff --git a/src/tfa/tfa_view.rs b/src/tfa/tfa_view.rs
index c4c1a43..bba9692 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<String>),
RemoveResult(Result<(), Error>),
@@ -104,11 +106,14 @@ pub enum ViewState {
#[doc(hidden)]
pub struct ProxmoxTfaView {
+ state: LoadableComponentState<ViewState>,
selection: Selection,
store: Store<TfaEntry>,
removing: bool,
}
+impl_deref_mut_property!(ProxmoxTfaView, state, LoadableComponentState<ViewState>);
+
impl ProxmoxTfaView {
fn get_selected_record(&self) -> Option<TfaEntry> {
let selected_key = self.selection.selected_key();
@@ -127,8 +132,12 @@ impl LoadableComponent for ProxmoxTfaView {
fn create(ctx: &LoadableComponentContext<Self>) -> 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<Self>, 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_message(Msg::RemoveResult(
delete_item(
base_url,
info.user_id.clone(),
@@ -213,7 +221,7 @@ impl LoadableComponent for ProxmoxTfaView {
password,
)
.await,
- )
+ ))
});
false
@@ -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,7 +304,7 @@ impl LoadableComponent for ProxmoxTfaView {
.selection(self.selection.clone())
.class("pwt-flex-fit")
.on_row_dblclick({
- let link = ctx.link();
+ let link = ctx.link().clone();
move |_: &mut _| link.send_message(Msg::Edit)
});
Mask::new(view).visible(self.removing).into()
@@ -314,7 +322,7 @@ impl LoadableComponent for ProxmoxTfaView {
TfaConfirmRemove::new(info)
.on_close(ctx.link().change_view_callback(|_| None))
.on_confirm({
- let link = ctx.link();
+ let link = ctx.link().clone();
move |password| link.send_message(Msg::Remove(password))
})
.into()
diff --git a/src/token_panel.rs b/src/token_panel.rs
index 031d54f..5291620 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<Vec<ApiToken>, Error> {
@@ -37,7 +38,7 @@ async fn load_api_tokens() -> Result<Vec<ApiToken>, Error> {
async fn create_token(
form_ctx: FormContext,
- link: LoadableComponentLink<ProxmoxTokenView>,
+ link: LoadableComponentScope<ProxmoxTokenView>,
) -> 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<ViewState>,
selection: Selection,
store: Store<ApiToken>,
columns: Rc<Vec<DataTableHeader<ApiToken>>>,
}
+crate::impl_deref_mut_property!(ProxmoxTokenView, state, LoadableComponentState<ViewState>);
+
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(),
@@ -217,7 +224,6 @@ impl LoadableComponent for ProxmoxTokenView {
fn update(&mut self, ctx: &LoadableComponentContext<Self>, 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<Self>) -> 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..f7af395 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<Vec<UserWithTokens>, Error> {
@@ -111,15 +112,17 @@ pub enum ViewState {
}
pub enum Msg {
- SelectionChange,
RemoveItem,
}
pub struct ProxmoxUserPanel {
+ state: LoadableComponentState<ViewState>,
store: Store<UserWithTokens>,
selection: Selection,
}
+impl_deref_mut_property!(ProxmoxUserPanel, state, LoadableComponentState<ViewState>);
+
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<Self>, 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);
@@ -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<Self>) -> 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
^ permalink raw reply [flat|nested] 2+ messages in thread
* [yew-devel] applied: [RFC yew-comp V2] refactor: move LoadableComponent state into component implementations
2025-12-10 9:28 [yew-devel] [RFC yew-comp V2] refactor: move LoadableComponent state into component implementations Dietmar Maurer
@ 2025-12-10 11:27 ` Thomas Lamprecht
0 siblings, 0 replies; 2+ messages in thread
From: Thomas Lamprecht @ 2025-12-10 11:27 UTC (permalink / raw)
To: yew-devel, Dietmar Maurer
On Wed, 10 Dec 2025 10:28:40 +0100, Dietmar Maurer wrote:
> 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`.
>
> [...]
Applied, thanks!
I squashed in a small fix for a left-over usage of send_custom_message in the
doc-comment example.
[1/1] refactor: move LoadableComponent state into component implementations
commit: 563050c58f1190f756349774998c083be5a3cd5d
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2025-12-10 11:28 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-12-10 9:28 [yew-devel] [RFC yew-comp V2] refactor: move LoadableComponent state into component implementations Dietmar Maurer
2025-12-10 11:27 ` [yew-devel] applied: " Thomas Lamprecht
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox