* [PATCH datacenter-manager/yew-widget-toolkit 0/3] ui: make the default dashboard configurable
@ 2026-08-20 8:17 Dominik Csapak
2026-08-20 8:17 ` [PATCH yew-widget-toolkit 1/3] state: event: add helpers for custom DOM events Dominik Csapak
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Dominik Csapak @ 2026-08-20 8:17 UTC (permalink / raw)
To: pdm-devel
Let the user select a view as the dashboard and persist that in the browser
local storage.
Note that for the pdm patches, pwt must be bumped and the new version must be
recorded in Cargo.toml.
proxmox-yew-widget-toolki:
Dominik Csapak (2):
state: event: add helpers for custom DOM events
state: persistent state: allow listening for updates
src/state/event.rs | 24 +++++++++++++++++++++++
src/state/mod.rs | 29 ++++++++++++++++++++++-----
src/state/persistent_state.rs | 37 ++++++++++++++++++++++++++++++++++-
3 files changed, 84 insertions(+), 6 deletions(-)
create mode 100644 src/state/event.rs
proxmox-datacenter-manager:
Dominik Csapak (1):
ui: make the default dashboard page configurable
ui/src/configuration/views.rs | 38 ++++++++++++++++++++++++++++++----
ui/src/main_menu.rs | 20 ++++++++++++++++--
ui/src/widget/mod.rs | 2 +-
ui/src/widget/view_selector.rs | 31 ++++++++++++++++++++++-----
4 files changed, 79 insertions(+), 12 deletions(-)
Summary over all repositories:
7 files changed, 163 insertions(+), 18 deletions(-)
--
Generated by murpp 0.11.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH yew-widget-toolkit 1/3] state: event: add helpers for custom DOM events
2026-08-20 8:17 [PATCH datacenter-manager/yew-widget-toolkit 0/3] ui: make the default dashboard configurable Dominik Csapak
@ 2026-08-20 8:17 ` Dominik Csapak
2026-08-20 8:17 ` [PATCH yew-widget-toolkit 2/3] state: persistent state: allow listening for updates Dominik Csapak
2026-08-20 8:17 ` [PATCH datacenter-manager 3/3] ui: make the default dashboard page configurable Dominik Csapak
2 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2026-08-20 8:17 UTC (permalink / raw)
To: pdm-devel
Creating and dispatching custom events is currently open coded, e.g. for
the `pwt-theme-changed` event. Provide small wrappers so that further
users do not have to repeat the window/document lookup and the JS error
conversion.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/state/event.rs | 24 ++++++++++++++++++++++++
src/state/mod.rs | 3 +++
2 files changed, 27 insertions(+)
create mode 100644 src/state/event.rs
diff --git a/src/state/event.rs b/src/state/event.rs
new file mode 100644
index 0000000..8566071
--- /dev/null
+++ b/src/state/event.rs
@@ -0,0 +1,24 @@
+//! Helpers to create and dispatch custom DOM events.
+
+use anyhow::{Error, format_err};
+use web_sys::Event;
+
+/// Create a custom [Event] with the given name.
+pub fn create_custom_event(name: &str) -> Result<Event, Error> {
+ Event::new(name).map_err(crate::convert_js_error)
+}
+
+/// Dispatch a custom event with the given name on the document.
+///
+/// The event is only delivered to listeners in the current document, so listeners in other
+/// browser tabs or windows are not notified.
+pub fn dispatch_custom_event(name: &str) -> Result<(), Error> {
+ let document = web_sys::window()
+ .and_then(|window| window.document())
+ .ok_or_else(|| format_err!("no document available"))?;
+
+ document
+ .dispatch_event(&create_custom_event(name)?)
+ .map(|_| ())
+ .map_err(crate::convert_js_error)
+}
diff --git a/src/state/mod.rs b/src/state/mod.rs
index 86d6b7f..6f99c7e 100644
--- a/src/state/mod.rs
+++ b/src/state/mod.rs
@@ -7,6 +7,9 @@ use serde::{Serialize, de::DeserializeOwned};
mod data_store;
pub use data_store::{DataNode, DataNodeDerefGuard, DataStore};
+mod event;
+pub use event::{create_custom_event, dispatch_custom_event};
+
mod loader;
pub use loader::{Loader, LoaderState};
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH yew-widget-toolkit 2/3] state: persistent state: allow listening for updates
2026-08-20 8:17 [PATCH datacenter-manager/yew-widget-toolkit 0/3] ui: make the default dashboard configurable Dominik Csapak
2026-08-20 8:17 ` [PATCH yew-widget-toolkit 1/3] state: event: add helpers for custom DOM events Dominik Csapak
@ 2026-08-20 8:17 ` Dominik Csapak
2026-08-20 8:17 ` [PATCH datacenter-manager 3/3] ui: make the default dashboard page configurable Dominik Csapak
2 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2026-08-20 8:17 UTC (permalink / raw)
To: pdm-devel
A `PersistentState` only reads its storage location on construction, so
instances held by unrelated components go stale as soon as another one
writes. Components deep in different subtrees often share nothing but the
storage key, so they cannot be handed a common instance either.
Dispatch a per-location event whenever `store_state` succeeds and let
`PersistentState` subscribe to it. The event carries no payload, as the
callback cannot mutate the instance it belongs to, hence `load` is now
public so that the owner can pick up the new value.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/state/mod.rs | 26 +++++++++++++++++++-----
src/state/persistent_state.rs | 37 ++++++++++++++++++++++++++++++++++-
2 files changed, 57 insertions(+), 6 deletions(-)
diff --git a/src/state/mod.rs b/src/state/mod.rs
index 6f99c7e..87ff8a2 100644
--- a/src/state/mod.rs
+++ b/src/state/mod.rs
@@ -124,6 +124,15 @@ pub fn load_state<T: 'static + DeserializeOwned>(storage: &StorageLocation) -> O
None
}
+fn create_storage_event_name(location: &StorageLocation) -> String {
+ let (prefix, name) = match location {
+ StorageLocation::Local(name) => ("local", name),
+ StorageLocation::Session(name) => ("session", name),
+ };
+
+ format!("pwt-storage-{prefix}-{name}")
+}
+
pub fn store_state<T: 'static + Serialize>(data: &T, storage: &StorageLocation) {
let (store, state_id) = match storage {
StorageLocation::Local(state_id) => (local_storage(), state_id),
@@ -131,11 +140,18 @@ pub fn store_state<T: 'static + Serialize>(data: &T, storage: &StorageLocation)
};
if let Some(store) = store {
let item_str = serde_json::to_string(data).unwrap();
- if let Err(err) = store.set_item(state_id, &item_str) {
- log::error!(
- "store persistent state {state_id} failed: {}",
- crate::convert_js_error(err)
- )
+ match store.set_item(state_id, &item_str) {
+ Ok(()) => {
+ if let Err(err) = dispatch_custom_event(&create_storage_event_name(storage)) {
+ log::error!("failed to send storage event: {err}")
+ }
+ }
+ Err(err) => {
+ log::error!(
+ "store persistent state {state_id} failed: {}",
+ crate::convert_js_error(err)
+ )
+ }
}
}
}
diff --git a/src/state/persistent_state.rs b/src/state/persistent_state.rs
index 90a54b7..672b0e5 100644
--- a/src/state/persistent_state.rs
+++ b/src/state/persistent_state.rs
@@ -1,7 +1,10 @@
+use gloo_events::EventListener;
use serde::{Serialize, de::DeserializeOwned};
use std::ops::Deref;
+use yew::Callback;
use crate::props::StorageLocation;
+use crate::state::create_storage_event_name;
/// Helper to store data persistently using window local [Storage](web_sys::Storage)
///
@@ -35,6 +38,7 @@ use crate::props::StorageLocation;
pub struct PersistentState<T> {
storage: StorageLocation,
data: T,
+ listener: Option<EventListener>,
}
impl<T> Deref for PersistentState<T> {
@@ -51,12 +55,14 @@ impl<T: 'static + Default + Serialize + DeserializeOwned> PersistentState<T> {
let mut me = Self {
data: T::default(),
storage: storage.into(),
+ listener: None,
};
me.load();
me
}
- fn load(&mut self) {
+ /// Loads the (potentially updated) data again from the given storage
+ pub fn load(&mut self) {
if let Some(data) = super::load_state(&self.storage) {
self.data = data;
}
@@ -80,4 +86,33 @@ impl<T: 'static + Default + Serialize + DeserializeOwned> PersistentState<T> {
pub fn into_inner(self) -> T {
self.data
}
+
+ /// Calls the callback when the data has changed.
+ ///
+ /// CAUTION: this does not update the data, [Self::load] must be called after this event to get
+ /// the current value.
+ ///
+ /// The callback also fires for updates done through this instance, so it must not write back
+ /// unconditionally, otherwise it triggers itself in an endless loop. Only updates from the
+ /// current document are reported, changes made in other browser tabs are not.
+ pub fn set_on_update(&mut self, on_update: impl Into<Callback<()>>) {
+ let on_update = on_update.into();
+ let Some(document) = web_sys::window().and_then(|window| window.document()) else {
+ return;
+ };
+
+ self.listener = Some(EventListener::new(
+ &document,
+ create_storage_event_name(&self.storage),
+ move |_| on_update.emit(()),
+ ));
+ }
+
+ /// Builder style method to set an update callback [Self::set_on_update].
+ /// CAUTION: this does not update the data, [Self::load] must be called after this event to
+ /// get the current value
+ pub fn on_update(mut self, on_update: impl Into<Callback<()>>) -> Self {
+ self.set_on_update(on_update);
+ self
+ }
}
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH datacenter-manager 3/3] ui: make the default dashboard page configurable
2026-08-20 8:17 [PATCH datacenter-manager/yew-widget-toolkit 0/3] ui: make the default dashboard configurable Dominik Csapak
2026-08-20 8:17 ` [PATCH yew-widget-toolkit 1/3] state: event: add helpers for custom DOM events Dominik Csapak
2026-08-20 8:17 ` [PATCH yew-widget-toolkit 2/3] state: persistent state: allow listening for updates Dominik Csapak
@ 2026-08-20 8:17 ` Dominik Csapak
2026-09-02 8:26 ` Lukas Wagner
2 siblings, 1 reply; 7+ messages in thread
From: Dominik Csapak @ 2026-08-20 8:17 UTC (permalink / raw)
To: pdm-devel
by allowing to set the user any view as their dashboard, saved in their
local browser storage.
It adds the selection combobox on the 'views' configuration page in the
toolbar.
Most of the code is just extending the ViewSelector so it can work in
other situations than the 'add view' dialog:
* add on_change property
* add dashboard_name property to show different text in different
contexts
* passing through the 'default' property from the combobos
It also refactors the '__dashboard__' string this does not have to be
hardcoded in different places.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/configuration/views.rs | 38 ++++++++++++++++++++++++++++++----
ui/src/main_menu.rs | 20 ++++++++++++++++--
ui/src/widget/mod.rs | 2 +-
ui/src/widget/view_selector.rs | 31 ++++++++++++++++++++++-----
4 files changed, 79 insertions(+), 12 deletions(-)
diff --git a/ui/src/configuration/views.rs b/ui/src/configuration/views.rs
index 8016b6d7..24b838c6 100644
--- a/ui/src/configuration/views.rs
+++ b/ui/src/configuration/views.rs
@@ -16,15 +16,16 @@ use proxmox_yew_comp::{
};
use pwt::prelude::*;
-use pwt::state::{Selection, Store};
+use pwt::props::StorageLocation;
+use pwt::state::{PersistentState, Selection, Store};
use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
use pwt::widget::form::{Checkbox, DisplayField, Field, FormContext};
-use pwt::widget::{Button, ConfirmDialog, InputPanel, Toolbar};
+use pwt::widget::{Button, ConfirmDialog, FieldLabel, InputPanel, Toolbar};
use pdm_api_types::views::{ViewConfig, ViewLayout, ViewTemplate};
use crate::ViewListContext;
-use crate::widget::{ViewFilterSelector, ViewSelector};
+use crate::widget::{DASHBOARD_VALUE, ViewFilterSelector, ViewSelector};
async fn create_view(
base_url: AttrValue,
@@ -38,7 +39,7 @@ async fn create_view(
description: String::new(),
layout: ViewLayout::Rows { rows: Vec::new() },
})?),
- "__dashboard__" => None,
+ crate::widget::DASHBOARD_VALUE => None,
layout => {
let store = store.read();
if let Some(config) = store.lookup_record(&Key::from(layout)) {
@@ -95,6 +96,7 @@ pub enum Msg {
LoadFinished(Vec<ViewConfig>),
Remove(Key),
Reload,
+ UpdateDefaultDashboard(String),
}
#[derive(PartialEq)]
@@ -110,6 +112,7 @@ pub struct ViewGridComp {
store: Store<ViewConfig>,
columns: Rc<Vec<DataTableHeader<ViewConfig>>>,
selection: Selection,
+ default_dashboard: PersistentState<Option<String>>,
}
pwt::impl_deref_mut_property!(ViewGridComp, state, LoadableComponentState<ViewState>);
@@ -213,6 +216,9 @@ impl LoadableComponent for ViewGridComp {
store: Store::with_extract_key(|config: &ViewConfig| config.id.as_str().into()),
columns: Self::columns(),
selection,
+ default_dashboard: PersistentState::<Option<String>>::new(StorageLocation::local(
+ "default-dashboard",
+ )),
}
}
@@ -253,6 +259,14 @@ impl LoadableComponent for ViewGridComp {
context.update_views();
}
}
+ Msg::UpdateDefaultDashboard(new_default) => {
+ if &new_default == DASHBOARD_VALUE {
+ self.default_dashboard.update(None);
+ } else {
+ self.default_dashboard.update(Some(new_default));
+ }
+ return false;
+ }
}
true
}
@@ -260,6 +274,11 @@ impl LoadableComponent for ViewGridComp {
fn toolbar(&self, ctx: &proxmox_yew_comp::LoadableComponentContext<Self>) -> Option<Html> {
let selection = self.selection.selected_key();
let link = ctx.link();
+ let default_dashboard = AttrValue::from(
+ self.default_dashboard
+ .clone()
+ .unwrap_or(DASHBOARD_VALUE.to_string()),
+ );
Some(
Toolbar::new()
.border_bottom(true)
@@ -277,6 +296,17 @@ impl LoadableComponent for ViewGridComp {
.disabled(selection.is_none())
.on_activate(link.change_view_callback(move |_| Some(ViewState::Remove))),
)
+ .with_spacer()
+ .with_child(FieldLabel::new(tr!("Dashboard")))
+ .with_child(
+ ViewSelector::new(self.store.clone())
+ .min_width(150)
+ .required(true)
+ .default(default_dashboard)
+ .dashboard_name(tr!("Default"))
+ .on_change(link.callback(Msg::UpdateDefaultDashboard)),
+ )
+ .with_flex_spacer()
.into(),
)
}
diff --git a/ui/src/main_menu.rs b/ui/src/main_menu.rs
index 1fddfa6a..e5253ef4 100644
--- a/ui/src/main_menu.rs
+++ b/ui/src/main_menu.rs
@@ -1,11 +1,12 @@
use std::rc::Rc;
use html::IntoPropValue;
+use pwt::props::StorageLocation;
use yew::virtual_dom::{Key, VComp, VNode};
use pwt::css::{self, Display, FlexFit};
use pwt::prelude::*;
-use pwt::state::{NavigationContextExt, Selection};
+use pwt::state::{NavigationContextExt, PersistentState, Selection};
use pwt::widget::nav::{Menu, MenuItem, NavigationDrawer};
use pwt::widget::{Container, Panel, Row, SelectionView, SelectionViewRenderInfo};
@@ -74,6 +75,7 @@ impl MainMenu {
pub enum Msg {
Select(Key),
UpdateAcl(AclContext),
+ UpdateDefaultDashboard,
}
pub struct PdmMainMenu {
@@ -81,6 +83,7 @@ pub struct PdmMainMenu {
menu_selection: Selection,
acl_context: AclContext,
_acl_context_listener: ContextHandle<AclContext>,
+ default_dashboard: PersistentState<Option<String>>,
}
fn register_view(
@@ -111,6 +114,7 @@ fn register_submenu(
view.add_builder(id, renderer);
menu.add_item(
MenuItem::new(text.into())
+ .default_collapsed(id == "views")
.key(id.to_string())
.icon_class(icon_class)
.submenu(submenu),
@@ -129,11 +133,17 @@ impl Component for PdmMainMenu {
.context(ctx.link().callback(Msg::UpdateAcl))
.expect("acl context not present");
+ let default_dashboard = PersistentState::<Option<String>>::new(StorageLocation::Local(
+ "default-dashboard".into(),
+ ))
+ .on_update(ctx.link().callback(|_| Msg::UpdateDefaultDashboard));
+
Self {
active: Key::from("dashboard"),
menu_selection: Selection::new(),
acl_context,
_acl_context_listener: acl_context_listener,
+ default_dashboard,
}
}
@@ -147,6 +157,10 @@ impl Component for PdmMainMenu {
self.acl_context = acl_context;
true
}
+ Msg::UpdateDefaultDashboard => {
+ self.default_dashboard.load();
+ true
+ }
}
}
@@ -168,13 +182,15 @@ impl Component for PdmMainMenu {
let mut menu = Menu::new();
+ let default_dashboard = self.default_dashboard.clone().map(|s| s.into());
+
register_view(
&mut menu,
&mut content,
tr!("Dashboard"),
"dashboard",
Some("fa fa-tachometer"),
- move |_| View::new(None).into(),
+ move |_| View::new(default_dashboard.clone()).into(),
);
let mut views = Menu::new();
diff --git a/ui/src/widget/mod.rs b/ui/src/widget/mod.rs
index 07c47e36..1eaf0ffd 100644
--- a/ui/src/widget/mod.rs
+++ b/ui/src/widget/mod.rs
@@ -31,7 +31,7 @@ pub use remote_selector::RemoteSelector;
mod remote_endpoint_selector;
mod view_selector;
-pub use view_selector::ViewSelector;
+pub use view_selector::{DASHBOARD_VALUE, ViewSelector};
mod view_filter_selector;
pub use view_filter_selector::ViewFilterSelector;
diff --git a/ui/src/widget/view_selector.rs b/ui/src/widget/view_selector.rs
index b48ef4f7..8bc15b0a 100644
--- a/ui/src/widget/view_selector.rs
+++ b/ui/src/widget/view_selector.rs
@@ -6,12 +6,29 @@ use pwt::widget::form::Combobox;
use pwt_macros::{builder, widget};
use pdm_api_types::views::ViewConfig;
+use yew::html::{IntoEventCallback, IntoPropValue};
+
+pub const DASHBOARD_VALUE: &str = "__dashboard__";
#[widget(comp=ViewSelectorComp, @input)]
#[derive(Clone, Properties, PartialEq)]
#[builder]
pub struct ViewSelector {
store: Store<ViewConfig>,
+
+ /// Change callback
+ #[builder_cb(IntoEventCallback, into_event_callback, String)]
+ #[prop_or_default]
+ pub on_change: Option<Callback<String>>,
+
+ /// The default value
+ #[builder(IntoPropValue, into_prop_value)]
+ #[prop_or_default]
+ pub default: Option<AttrValue>,
+
+ #[builder]
+ #[prop_or(tr!("Dashboard"))]
+ pub dashboard_name: String,
}
impl ViewSelector {
@@ -32,19 +49,23 @@ impl Component for ViewSelectorComp {
}
fn view(&self, ctx: &Context<Self>) -> Html {
- let mut list = vec!["__dashboard__".into()];
+ let props = ctx.props();
+ let mut list = vec![DASHBOARD_VALUE.into()];
let store = &ctx.props().store;
for item in store.read().data().iter() {
list.push(item.id.clone().into());
}
Combobox::new()
- .items(Rc::new(list))
+ .with_std_props(&ctx.props().std_props)
.with_input_props(&ctx.props().input_props)
- .on_change(|_| {})
+ .items(Rc::new(list))
+ .on_change(props.on_change.clone())
+ .default(props.default.clone())
.render_value({
+ let name = props.dashboard_name.clone();
move |value: &AttrValue| {
- if value == "__dashboard__" {
- html! {{tr!("Dashboard")}}
+ if value == DASHBOARD_VALUE {
+ html! {{&name}}
} else {
html! {{value}}
}
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH datacenter-manager 3/3] ui: make the default dashboard page configurable
2026-08-20 8:17 ` [PATCH datacenter-manager 3/3] ui: make the default dashboard page configurable Dominik Csapak
@ 2026-09-02 8:26 ` Lukas Wagner
2026-09-02 11:10 ` Dominik Csapak
0 siblings, 1 reply; 7+ messages in thread
From: Lukas Wagner @ 2026-09-02 8:26 UTC (permalink / raw)
To: Dominik Csapak, pdm-devel
Thanks for these patches!
On Thu Aug 20, 2026 at 10:17 AM CEST, Dominik Csapak wrote:
> by allowing to set the user any view as their dashboard, saved in their
> local browser storage.
>
> It adds the selection combobox on the 'views' configuration page in the
> toolbar.
>
>From a user's perspective, I think having such a setting presented this
way is a bit confusing, since there is no hint about this being a
browser-local setting. If I didn't read the patch notes, my instinct
while exploring the UI on my own would be that this is a global setting
that affects all sessions and all users.
Adding a hint for this in the toolbar, either as a label or tooltip
seems a bit inpractical to me, mostly due to space constraints for
a label or discoverability for a tooltip.
Some alternatives could be:
- Replace the combobox in the toolbar with a button, e.g. "Configure
Dashboard", pressing that button shows a dialog that allows selecting
a view, while also explaining that this is a local setting.
I would position this button on the right-hand side, to differentiate
it from the 'entity-management' buttons add/modify/delete.
- The dashboard already has a couple local settings, such as the refresh
interval, etc., these are in the 'gear' menu in the toolbar. Maybe
on could select a view in there?
Would in general also not hurt to add a note there that all of these
settings are local
- Have a 'My Settings' dialog in the user menu similar to PVE
- Abandon the local setting altogether and make it a per-user setting
that is stored in the backend. Might require bigger changes, since I
don't think we have any good way to store per-user properties right
now. Users that routinely use multiple browsers and/or devices might
prefer this over a local setting.
What do you think?
One further thing that I have noticed: If a view that is used as a
dashboard is deleted, we should probably fall back to the default
dashboard again. Otherwise user might complain about 'broken' dashboards
without a clear indication about what is wrong.
> Most of the code is just extending the ViewSelector so it can work in
> other situations than the 'add view' dialog:
> * add on_change property
> * add dashboard_name property to show different text in different
> contexts
> * passing through the 'default' property from the combobos
>
> It also refactors the '__dashboard__' string this does not have to be
> hardcoded in different places.
>
[snip]
> diff --git a/ui/src/widget/mod.rs b/ui/src/widget/mod.rs
> index 07c47e36..1eaf0ffd 100644
> --- a/ui/src/widget/mod.rs
> +++ b/ui/src/widget/mod.rs
> @@ -31,7 +31,7 @@ pub use remote_selector::RemoteSelector;
> mod remote_endpoint_selector;
>
> mod view_selector;
> -pub use view_selector::ViewSelector;
> +pub use view_selector::{DASHBOARD_VALUE, ViewSelector};
>
> mod view_filter_selector;
> pub use view_filter_selector::ViewFilterSelector;
> diff --git a/ui/src/widget/view_selector.rs b/ui/src/widget/view_selector.rs
> index b48ef4f7..8bc15b0a 100644
> --- a/ui/src/widget/view_selector.rs
> +++ b/ui/src/widget/view_selector.rs
> @@ -6,12 +6,29 @@ use pwt::widget::form::Combobox;
> use pwt_macros::{builder, widget};
>
> use pdm_api_types::views::ViewConfig;
> +use yew::html::{IntoEventCallback, IntoPropValue};
> +
> +pub const DASHBOARD_VALUE: &str = "__dashboard__";
Do you think it would be possible to get this working without such a
magic value? So rather, in the code, instead of having a String for the
view ID, have a Option<String> where None is the default dashboard and
Some(id) is a specific view?
If that is impractical, we should probably disallow views with this
specific name from being created in the backend. I managed to get some
odd behavior after creating a view with this specific name.
Probably a bit unlikely for a user to create a view with this specific
name by accident, but if we can avoid having conflicts/issues altogether
with some changes in the code, that would of course be better.
>
> #[widget(comp=ViewSelectorComp, @input)]
> #[derive(Clone, Properties, PartialEq)]
> #[builder]
> pub struct ViewSelector {
> store: Store<ViewConfig>,
> +
> + /// Change callback
> + #[builder_cb(IntoEventCallback, into_event_callback, String)]
> + #[prop_or_default]
> + pub on_change: Option<Callback<String>>,
> +
> + /// The default value
> + #[builder(IntoPropValue, into_prop_value)]
> + #[prop_or_default]
> + pub default: Option<AttrValue>,
> +
> + #[builder]
> + #[prop_or(tr!("Dashboard"))]
> + pub dashboard_name: String,
> }
>
> impl ViewSelector {
> @@ -32,19 +49,23 @@ impl Component for ViewSelectorComp {
> }
>
> fn view(&self, ctx: &Context<Self>) -> Html {
> - let mut list = vec!["__dashboard__".into()];
> + let props = ctx.props();
> + let mut list = vec![DASHBOARD_VALUE.into()];
> let store = &ctx.props().store;
> for item in store.read().data().iter() {
> list.push(item.id.clone().into());
> }
> Combobox::new()
> - .items(Rc::new(list))
> + .with_std_props(&ctx.props().std_props)
> .with_input_props(&ctx.props().input_props)
> - .on_change(|_| {})
> + .items(Rc::new(list))
> + .on_change(props.on_change.clone())
> + .default(props.default.clone())
> .render_value({
> + let name = props.dashboard_name.clone();
> move |value: &AttrValue| {
> - if value == "__dashboard__" {
> - html! {{tr!("Dashboard")}}
> + if value == DASHBOARD_VALUE {
> + html! {{&name}}
> } else {
> html! {{value}}
> }
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH datacenter-manager 3/3] ui: make the default dashboard page configurable
2026-09-02 8:26 ` Lukas Wagner
@ 2026-09-02 11:10 ` Dominik Csapak
2026-09-02 11:54 ` Lukas Wagner
0 siblings, 1 reply; 7+ messages in thread
From: Dominik Csapak @ 2026-09-02 11:10 UTC (permalink / raw)
To: Lukas Wagner, pdm-devel
On 9/2/26 10:26 AM, Lukas Wagner wrote:
> Thanks for these patches!
>
Thanks for looking at them!
> On Thu Aug 20, 2026 at 10:17 AM CEST, Dominik Csapak wrote:
>> by allowing to set the user any view as their dashboard, saved in their
>> local browser storage.
>>
>> It adds the selection combobox on the 'views' configuration page in the
>> toolbar.
>>
>
> From a user's perspective, I think having such a setting presented this
> way is a bit confusing, since there is no hint about this being a
> browser-local setting. If I didn't read the patch notes, my instinct
> while exploring the UI on my own would be that this is a global setting
> that affects all sessions and all users.
Yeah that make sense.
>
> Adding a hint for this in the toolbar, either as a label or tooltip
> seems a bit inpractical to me, mostly due to space constraints for
> a label or discoverability for a tooltip.
>
True.
> Some alternatives could be:
>
> - Replace the combobox in the toolbar with a button, e.g. "Configure
> Dashboard", pressing that button shows a dialog that allows selecting
> a view, while also explaining that this is a local setting.
> I would position this button on the right-hand side, to differentiate
> it from the 'entity-management' buttons add/modify/delete.
probably the easiest to implement but IMO not the best option.
>
> - The dashboard already has a couple local settings, such as the refresh
> interval, etc., these are in the 'gear' menu in the toolbar. Maybe
> on could select a view in there?
> Would in general also not hurt to add a note there that all of these
> settings are local
well that would convert the 'refresh configuration' into 'dashboard
configuration' but only for the main dashboard. so we'd have
nearly identical settings for the dashboard and a individual view
without any hint from the outside.
I don't think the low discoverability is good ...
>
> - Have a 'My Settings' dialog in the user menu similar to PVE
I would probably opt for this, since we already are missing
some settings that would be nice here (e.g. xterm.js/novnc settings
that we already have in PVE)
>
> - Abandon the local setting altogether and make it a per-user setting
> that is stored in the backend. Might require bigger changes, since I
> don't think we have any good way to store per-user properties right
> now. Users that routinely use multiple browsers and/or devices might
> prefer this over a local setting.
>
One of the biggest thing that makes me a bit hesitant to do this
is that we don't usually have per user backend (gui) settings at all
If we do want those here, I'd also like them on the PVE side
(e.g. tree/view settings)
>
> What do you think?
see above, currently I'd prefer to implementing a 'my settings' window.
another option would be to have the global default set in the backend
but this clashes a bit with the permissions per view etc...
>
> One further thing that I have noticed: If a view that is used as a
> dashboard is deleted, we should probably fall back to the default
> dashboard again. Otherwise user might complain about 'broken' dashboards
> without a clear indication about what is wrong.
yes that sounds sensible (at least as long as we only save it in
the browser local storage. if we have a setting in the backend
we could prevent deletion if anybody is using that or reset
all users preferences)
>
>
>> Most of the code is just extending the ViewSelector so it can work in
>> other situations than the 'add view' dialog:
>> * add on_change property
>> * add dashboard_name property to show different text in different
>> contexts
>> * passing through the 'default' property from the combobos
>>
>> It also refactors the '__dashboard__' string this does not have to be
>> hardcoded in different places.
>>
>
> [snip]
>
>> diff --git a/ui/src/widget/mod.rs b/ui/src/widget/mod.rs
>> index 07c47e36..1eaf0ffd 100644
>> --- a/ui/src/widget/mod.rs
>> +++ b/ui/src/widget/mod.rs
>> @@ -31,7 +31,7 @@ pub use remote_selector::RemoteSelector;
>> mod remote_endpoint_selector;
>>
>> mod view_selector;
>> -pub use view_selector::ViewSelector;
>> +pub use view_selector::{DASHBOARD_VALUE, ViewSelector};
>>
>> mod view_filter_selector;
>> pub use view_filter_selector::ViewFilterSelector;
>> diff --git a/ui/src/widget/view_selector.rs b/ui/src/widget/view_selector.rs
>> index b48ef4f7..8bc15b0a 100644
>> --- a/ui/src/widget/view_selector.rs
>> +++ b/ui/src/widget/view_selector.rs
>> @@ -6,12 +6,29 @@ use pwt::widget::form::Combobox;
>> use pwt_macros::{builder, widget};
>>
>> use pdm_api_types::views::ViewConfig;
>> +use yew::html::{IntoEventCallback, IntoPropValue};
>> +
>> +pub const DASHBOARD_VALUE: &str = "__dashboard__";
>
>
> Do you think it would be possible to get this working without such a
> magic value? So rather, in the code, instead of having a String for the
> view ID, have a Option<String> where None is the default dashboard and
> Some(id) is a specific view?
>
> If that is impractical, we should probably disallow views with this
> specific name from being created in the backend. I managed to get some
> odd behavior after creating a view with this specific name.
>
> Probably a bit unlikely for a user to create a view with this specific
> name by accident, but if we can avoid having conflicts/issues altogether
> with some changes in the code, that would of course be better.
Yes I'll look at doing that. This happens when one works too much with
JS 🤪
>
>>
>> #[widget(comp=ViewSelectorComp, @input)]
>> #[derive(Clone, Properties, PartialEq)]
>> #[builder]
>> pub struct ViewSelector {
>> store: Store<ViewConfig>,
>> +
>> + /// Change callback
>> + #[builder_cb(IntoEventCallback, into_event_callback, String)]
>> + #[prop_or_default]
>> + pub on_change: Option<Callback<String>>,
>> +
>> + /// The default value
>> + #[builder(IntoPropValue, into_prop_value)]
>> + #[prop_or_default]
>> + pub default: Option<AttrValue>,
>> +
>> + #[builder]
>> + #[prop_or(tr!("Dashboard"))]
>> + pub dashboard_name: String,
>> }
>>
>> impl ViewSelector {
>> @@ -32,19 +49,23 @@ impl Component for ViewSelectorComp {
>> }
>>
>> fn view(&self, ctx: &Context<Self>) -> Html {
>> - let mut list = vec!["__dashboard__".into()];
>> + let props = ctx.props();
>> + let mut list = vec![DASHBOARD_VALUE.into()];
>> let store = &ctx.props().store;
>> for item in store.read().data().iter() {
>> list.push(item.id.clone().into());
>> }
>> Combobox::new()
>> - .items(Rc::new(list))
>> + .with_std_props(&ctx.props().std_props)
>> .with_input_props(&ctx.props().input_props)
>> - .on_change(|_| {})
>> + .items(Rc::new(list))
>> + .on_change(props.on_change.clone())
>> + .default(props.default.clone())
>> .render_value({
>> + let name = props.dashboard_name.clone();
>> move |value: &AttrValue| {
>> - if value == "__dashboard__" {
>> - html! {{tr!("Dashboard")}}
>> + if value == DASHBOARD_VALUE {
>> + html! {{&name}}
>> } else {
>> html! {{value}}
>> }
>
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH datacenter-manager 3/3] ui: make the default dashboard page configurable
2026-09-02 11:10 ` Dominik Csapak
@ 2026-09-02 11:54 ` Lukas Wagner
0 siblings, 0 replies; 7+ messages in thread
From: Lukas Wagner @ 2026-09-02 11:54 UTC (permalink / raw)
To: Dominik Csapak, Lukas Wagner, pdm-devel
On Wed Sep 2, 2026 at 1:10 PM CEST, Dominik Csapak wrote:
>>
>> - Have a 'My Settings' dialog in the user menu similar to PVE
>
> I would probably opt for this, since we already are missing
> some settings that would be nice here (e.g. xterm.js/novnc settings
> that we already have in PVE)
>
Seems good to me!
>>
>> - Abandon the local setting altogether and make it a per-user setting
>> that is stored in the backend. Might require bigger changes, since I
>> don't think we have any good way to store per-user properties right
>> now. Users that routinely use multiple browsers and/or devices might
>> prefer this over a local setting.
>>
>
> One of the biggest thing that makes me a bit hesitant to do this
> is that we don't usually have per user backend (gui) settings at all
>
> If we do want those here, I'd also like them on the PVE side
> (e.g. tree/view settings)
>
Yeah, it would definitely a much bigger undertaking. One where it
definitely makes sense to think about the cross-product implications.
I guess storing these settings in local storage *now* does not really stop
us from changing it later to a per-user, backend setting, if we think
that this would be a good idea, so no need to overthink this right now.
>>
>> What do you think?
>
> see above, currently I'd prefer to implementing a 'my settings' window.
>
> another option would be to have the global default set in the backend
> but this clashes a bit with the permissions per view etc...
>
>>
>> One further thing that I have noticed: If a view that is used as a
>> dashboard is deleted, we should probably fall back to the default
>> dashboard again. Otherwise user might complain about 'broken' dashboards
>> without a clear indication about what is wrong.
>
> yes that sounds sensible (at least as long as we only save it in
> the browser local storage. if we have a setting in the backend
> we could prevent deletion if anybody is using that or reset
> all users preferences)
>
Maybe the UI could then also show a small hint in the header, maybe
something like
"View xyz does not exist, showing default dashboard instead"
I guess if there is such a hint, we could also just shown an empty view
instead of the default dashboard, since then the user knows what's going
on.
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-02 11:55 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-20 8:17 [PATCH datacenter-manager/yew-widget-toolkit 0/3] ui: make the default dashboard configurable Dominik Csapak
2026-08-20 8:17 ` [PATCH yew-widget-toolkit 1/3] state: event: add helpers for custom DOM events Dominik Csapak
2026-08-20 8:17 ` [PATCH yew-widget-toolkit 2/3] state: persistent state: allow listening for updates Dominik Csapak
2026-08-20 8:17 ` [PATCH datacenter-manager 3/3] ui: make the default dashboard page configurable Dominik Csapak
2026-09-02 8:26 ` Lukas Wagner
2026-09-02 11:10 ` Dominik Csapak
2026-09-02 11:54 ` Lukas Wagner
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.