[pdm-devel] [RFC pdm v2] ui: make layout more consistent using new ContentSpacer widget

Dietmar Maurer dietmar at proxmox.com
Fri Jan 10 13:50:23 CET 2025


Signed-off-by: Dietmar Maurer <dietmar at proxmox.com>
---

 Changhes in v2:
 - use box shadows instead of border
 - use ContentSpacer for dashboard
 - use ContentSpacer for PveRemote

 ui/css/crisp-yew-style.scss            | 12 ++++
 ui/css/desktop-yew-style.scss          | 11 +++
 ui/css/pdm.scss                        | 26 ++++++-
 ui/src/administration/mod.rs           | 28 ++++++--
 ui/src/certificates.rs                 | 30 ++++++--
 ui/src/configuration/mod.rs            | 25 ++++---
 ui/src/configuration/other/mod.rs      |  9 ++-
 ui/src/configuration/other/webauthn.rs |  1 -
 ui/src/dashboard/mod.rs                | 17 +++--
 ui/src/main_menu.rs                    | 22 +++---
 ui/src/pve/mod.rs                      |  7 +-
 ui/src/widget/content_spacer.rs        | 99 ++++++++++++++++++++++++++
 ui/src/widget/mod.rs                   |  3 +
 13 files changed, 242 insertions(+), 48 deletions(-)
 create mode 100644 ui/src/widget/content_spacer.rs

diff --git a/ui/css/crisp-yew-style.scss b/ui/css/crisp-yew-style.scss
index d254b53..df8bb9c 100644
--- a/ui/css/crisp-yew-style.scss
+++ b/ui/css/crisp-yew-style.scss
@@ -1,2 +1,14 @@
 @import "../pwt-assets/scss/crisp-yew-style";
 @import "pdm";
+
+.proxmox-content-spacer {
+    @include color-scheme-vars("neutral");
+
+    &.proxmox-content-spacer-with-one-child {
+        padding: 0;
+    }
+
+    &.proxmox-content-spacer-with-one-child > * {
+        border: 0;
+    }
+}
diff --git a/ui/css/desktop-yew-style.scss b/ui/css/desktop-yew-style.scss
index 3d1ac0a..2fb1832 100644
--- a/ui/css/desktop-yew-style.scss
+++ b/ui/css/desktop-yew-style.scss
@@ -1,2 +1,13 @@
 @import "../pwt-assets/scss/desktop-yew-style";
 @import "pdm";
+
+.proxmox-content-spacer {
+    padding: var(--pwt-spacer-4);
+    gap: var(--pwt-spacer-4);
+
+    & > * {
+        border: 0px !important;
+        border-top: 1px solid var(--pwt-color-surface) !important;
+        @include elevation-box-shadow(1);
+    }
+}
diff --git a/ui/css/pdm.scss b/ui/css/pdm.scss
index 4ef22e2..6597f31 100644
--- a/ui/css/pdm.scss
+++ b/ui/css/pdm.scss
@@ -60,7 +60,27 @@
 }
 
 :root.pwt-dark-mode {
-  .fa-memory, .fa-cpu {
-    filter: invert(90%);
-  }
+    .fa-memory,
+    .fa-cpu {
+        filter: invert(90%);
+    }
+}
+
+.proxmox-content-spacer {
+    @include color-scheme-vars("surface");
+    color: var(--pwt-color);
+    background-color: var(--pwt-color-background);
+
+    padding: var(--pwt-spacer-2);
+    gap: var(--pwt-spacer-2);
+
+    display: flex;
+    flex-direction: column;
+
+    & > * {
+        @include color-scheme-vars("neutral");
+        border: 1px solid var(--pwt-color-border);
+        color: var(--pwt-color);
+        background-color: var(--pwt-color-background);
+    }
 }
diff --git a/ui/src/administration/mod.rs b/ui/src/administration/mod.rs
index 15d04ae..eeb9efa 100644
--- a/ui/src/administration/mod.rs
+++ b/ui/src/administration/mod.rs
@@ -18,6 +18,8 @@ use pwt_macros::builder;
 
 use proxmox_yew_comp::{AptPackageManager, AptRepositories, ExistingProduct, Syslog, Tasks};
 
+use crate::widget::ContentSpacer;
+
 #[derive(Clone, PartialEq, Properties)]
 #[builder]
 pub struct ServerAdministration {
@@ -73,8 +75,9 @@ impl Component for PdmServerAdministration {
                     .label("Updates")
                     .icon_class("fa fa-refresh"),
                 move |_| {
-                    AptPackageManager::new()
-                        .enable_upgrade(enable_upgrade)
+                    ContentSpacer::new()
+                        .class("pwt-flex-fill")
+                        .with_child(AptPackageManager::new().enable_upgrade(enable_upgrade))
                         .into()
                 },
             )
@@ -83,21 +86,36 @@ impl Component for PdmServerAdministration {
                     .key("repositories")
                     .label("Repositories")
                     .icon_class("fa fa-files-o"),
-                |_| AptRepositories::new().product(ExistingProduct::PDM).into(),
+                |_| {
+                    ContentSpacer::new()
+                        .class("pwt-flex-fit")
+                        .with_child(AptRepositories::new().product(ExistingProduct::PDM))
+                        .into()
+                },
             )
             .with_item_builder(
                 TabBarItem::new()
                     .key("syslog")
                     .label("Syslog")
                     .icon_class("fa fa-list"),
-                |_| Syslog::new().into(), // fixme: use JournalView instead?
+                |_| {
+                    ContentSpacer::new()
+                        .class("pwt-flex-fit")
+                        .with_child(Syslog::new())
+                        .into() // fixme: use JournalView instead?
+                },
             )
             .with_item_builder(
                 TabBarItem::new()
                     .key("tasks")
                     .label("Tasks")
                     .icon_class("fa fa-list-alt"),
-                |_| Tasks::new().into(),
+                |_| {
+                    ContentSpacer::new()
+                        .class("pwt-flex-fit")
+                        .with_child(Tasks::new())
+                        .into()
+                },
             );
 
         NavigationContainer::new().with_child(panel).into()
diff --git a/ui/src/certificates.rs b/ui/src/certificates.rs
index 39929b4..d348785 100644
--- a/ui/src/certificates.rs
+++ b/ui/src/certificates.rs
@@ -7,6 +7,8 @@ use proxmox_yew_comp::acme::{
     AcmeAccountsPanel, AcmeDomainsPanel, AcmePluginsPanel, CertificateList,
 };
 
+use crate::widget::ContentSpacer;
+
 #[function_component(CertificatesPanel)]
 pub fn certificates_panel() -> Html {
     let panel = TabPanel::new()
@@ -19,23 +21,43 @@ pub fn certificates_panel() -> Html {
             TabBarItem::new()
                 .key("certificate_List")
                 .label("Certificates"),
-            |_| CertificateList::new().into(),
+            |_| {
+                ContentSpacer::new()
+                    .class("pwt-flex-fit")
+                    .with_child(CertificateList::new())
+                    .into()
+            },
         )
         .with_item_builder(
             TabBarItem::new().key("acme_domains").label("ACME Domains"),
-            |_| AcmeDomainsPanel::new().url("/config/certificate").into(),
+            |_| {
+                ContentSpacer::new()
+                    .class("pwt-flex-fit")
+                    .with_child(AcmeDomainsPanel::new().url("/config/certificate"))
+                    .into()
+            },
         )
         .with_item_builder(
             TabBarItem::new()
                 .key("acme_accounts")
                 .label("ACME Accounts"),
-            |_| AcmeAccountsPanel::new().into(),
+            |_| {
+                ContentSpacer::new()
+                    .class("pwt-flex-fit")
+                    .with_child(AcmeAccountsPanel::new())
+                    .into()
+            },
         )
         .with_item_builder(
             TabBarItem::new()
                 .key("acme_plugins")
                 .label("Challenge Plugins"),
-            |_| AcmePluginsPanel::new().into(),
+            |_| {
+                ContentSpacer::new()
+                    .class("pwt-flex-fit")
+                    .with_child(AcmePluginsPanel::new())
+                    .into()
+            },
         );
 
     NavigationContainer::new().with_child(panel).into()
diff --git a/ui/src/configuration/mod.rs b/ui/src/configuration/mod.rs
index 15421a4..63e741e 100644
--- a/ui/src/configuration/mod.rs
+++ b/ui/src/configuration/mod.rs
@@ -1,7 +1,7 @@
 use pwt::prelude::*;
 use pwt::props::StorageLocation;
 use pwt::state::NavigationContainer;
-use pwt::widget::{Column, MiniScrollMode, Panel, TabBarItem, TabPanel};
+use pwt::widget::{Column, Container, MiniScrollMode, Panel, TabBarItem, TabPanel};
 
 use proxmox_yew_comp::configuration::TimePanel;
 use proxmox_yew_comp::configuration::{DnsPanel, NetworkView};
@@ -11,6 +11,8 @@ use proxmox_yew_comp::UserPanel;
 mod other;
 pub use other::OtherPanel;
 
+use crate::widget::ContentSpacer;
+
 #[function_component(SystemConfiguration)]
 pub fn system_configuration() -> Html {
     let panel = TabPanel::new()
@@ -50,14 +52,24 @@ pub fn access_control() -> Html {
                 .key("user-management")
                 .icon_class("fa fa-user")
                 .label(tr!("User Management")),
-            |_| UserPanel::new().into(),
+            |_| {
+                ContentSpacer::new()
+                    .class("pwt-flex-fit")
+                    .with_child(UserPanel::new())
+                    .into()
+            },
         )
         .with_item_builder(
             TabBarItem::new()
                 .key("two-factor")
                 .icon_class("fa fa-key")
                 .label(tr!("Two Factor Authentication")),
-            |_| TfaView::new().into(),
+            |_| {
+                ContentSpacer::new()
+                    .class("pwt-flex-fit")
+                    .with_child(TfaView::new())
+                    .into()
+            },
         );
 
     NavigationContainer::new().with_child(panel).into()
@@ -65,19 +77,15 @@ pub fn access_control() -> Html {
 
 #[function_component(NetworkTimePanel)]
 pub fn create_network_time_panel() -> Html {
-    Column::new()
+    ContentSpacer::new()
         .class("pwt-flex-fit")
-        .padding(2)
-        .gap(4)
         .with_child(
             Panel::new()
-                .border(true)
                 .title(tr!("Time"))
                 .with_child(html! { <TimePanel/> }),
         )
         .with_child(
             Panel::new()
-                .border(true)
                 .title(tr!("DNS"))
                 .with_child(html! { <DnsPanel/> }),
         )
@@ -85,7 +93,6 @@ pub fn create_network_time_panel() -> Html {
             Panel::new()
                 .min_height(200)
                 .class("pwt-flex-fit")
-                .border(true)
                 .title(tr!("Network Interfaces"))
                 .with_child(NetworkView::new()),
         )
diff --git a/ui/src/configuration/other/mod.rs b/ui/src/configuration/other/mod.rs
index f2435ac..cdd8e98 100644
--- a/ui/src/configuration/other/mod.rs
+++ b/ui/src/configuration/other/mod.rs
@@ -1,14 +1,13 @@
 use pwt::prelude::*;
-use pwt::widget::Column;
+
+use crate::widget::ContentSpacer;
 
 mod webauthn;
 
 #[function_component(OtherPanel)]
 pub fn create_other_panel() -> Html {
-    Column::new()
-        .class("pwt-flex-fill")
-        .padding(2)
-        .gap(4)
+    ContentSpacer::new()
+        .class("pwt-flex-fit")
         .with_child(html! { <webauthn::WebauthnPanel/> })
         .into()
 }
diff --git a/ui/src/configuration/other/webauthn.rs b/ui/src/configuration/other/webauthn.rs
index 08bc09a..9748e3c 100644
--- a/ui/src/configuration/other/webauthn.rs
+++ b/ui/src/configuration/other/webauthn.rs
@@ -19,7 +19,6 @@ use proxmox_yew_comp::{ObjectGrid, ObjectGridRow};
 #[function_component(WebauthnPanel)]
 pub fn webauthn_panel() -> Html {
     Panel::new()
-        .border(true)
         .title(tr!("WebAuthn TFA"))
         .with_child(object_grid())
         .into()
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index fe7ac1b..49239b2 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -18,7 +18,7 @@ use pwt::{
 use pdm_api_types::resource::{GuestStatusCount, NodeStatusCount, ResourcesStatus};
 use pdm_client::types::TopEntity;
 
-use crate::{remotes::AddWizard, RemoteList};
+use crate::{remotes::AddWizard, widget::ContentSpacer, RemoteList};
 
 mod top_entities;
 pub use top_entities::TopEntities;
@@ -275,17 +275,15 @@ impl Component for PdmDashboard {
 
         let content = Column::new()
             .class(FlexFit)
-            .padding(4)
-            .gap(4)
             .with_child(
-                Row::new()
-                    .gap(4)
+                ContentSpacer::new()
+                    .class("pwt-flex-direction-row")
                     .class(FlexWrap::Wrap)
                     .with_child(
                         Panel::new()
                             .title(self.create_title_with_icon("server", tr!("Remotes")))
                             .flex(1.0)
-                            .border(true)
+                            //.border(true)
                             .width(300)
                             .min_height(175)
                             .with_tool(
@@ -389,10 +387,11 @@ impl Component for PdmDashboard {
                     .with_child(SubscriptionInfo::new()),
             )
             .with_child(
-                Row::new()
-                    .gap(4)
+                ContentSpacer::new()
+                    .class("pwt-flex-direction-row")
+                    .style("padding-top", "0")
                     .class(FlexWrap::Wrap)
-                    .min_height(175)
+                    //.min_height(175)
                     .with_child(self.create_top_entities_panel(
                         "desktop",
                         tr!("Guests With the Highest CPU Usage"),
diff --git a/ui/src/main_menu.rs b/ui/src/main_menu.rs
index e75eb90..3d2b5a0 100644
--- a/ui/src/main_menu.rs
+++ b/ui/src/main_menu.rs
@@ -16,6 +16,7 @@ use proxmox_yew_comp::{NotesView, XTermJs};
 
 use pdm_api_types::remotes::RemoteType;
 
+use crate::widget::ContentSpacer;
 use crate::{
     AccessControl, CertificatesPanel, Dashboard, RemoteConfigPanel, RemoteList,
     ServerAdministration, SystemConfiguration,
@@ -177,14 +178,14 @@ impl Component for PdmMainMenu {
             "notes",
             Some("fa fa-sticky-note-o"),
             move |_| {
-                NotesView::new("/config/notes")
-                    .on_submit(|notes| async move {
-                        proxmox_yew_comp::http_put(
-                            "/config/notes",
-                            Some(serde_json::to_value(&notes)?),
-                        )
+                let notes = NotesView::new("/config/notes").on_submit(|notes| async move {
+                    proxmox_yew_comp::http_put("/config/notes", Some(serde_json::to_value(&notes)?))
                         .await
-                    })
+                });
+
+                ContentSpacer::new()
+                    .class("pwt-flex-fit")
+                    .with_child(notes)
                     .into()
             },
         );
@@ -274,7 +275,12 @@ impl Component for PdmMainMenu {
             } else {
                 "fa fa-server"
             }),
-            |_| RemoteConfigPanel::new().into(),
+            |_| {
+                ContentSpacer::new()
+                    .class("pwt-flex-fit")
+                    .with_child(RemoteConfigPanel::new())
+                    .into()
+            },
             remote_submenu,
         );
 
diff --git a/ui/src/pve/mod.rs b/ui/src/pve/mod.rs
index 7925bbf..bde89b5 100644
--- a/ui/src/pve/mod.rs
+++ b/ui/src/pve/mod.rs
@@ -31,7 +31,7 @@ pub mod utils;
 mod tree;
 use tree::PveTreeNode;
 
-use crate::get_deep_url;
+use crate::{get_deep_url, widget::ContentSpacer};
 
 #[derive(Debug, Eq, PartialEq, Properties)]
 pub struct PveRemote {
@@ -163,10 +163,9 @@ impl LoadableComponent for PveRemoteComp {
             .with_child(tr! {"Remote '{0}'", ctx.props().remote})
             .into();
 
-        let content = Row::new()
+        let content = ContentSpacer::new()
             .class(FlexFit)
-            .padding(4)
-            .gap(4)
+            .class("pwt-flex-direction-row")
             .with_child(
                 Panel::new()
                     .min_width(500)
diff --git a/ui/src/widget/content_spacer.rs b/ui/src/widget/content_spacer.rs
new file mode 100644
index 0000000..575e332
--- /dev/null
+++ b/ui/src/widget/content_spacer.rs
@@ -0,0 +1,99 @@
+use std::rc::Rc;
+
+use pwt::props::{AsClassesMut, AsCssStylesMut, CssStyles};
+use pwt::widget::Container;
+use yew::prelude::*;
+use yew::virtual_dom::{Key, VComp, VNode};
+
+use pwt::prelude::*;
+
+#[derive(Clone, PartialEq, Properties)]
+pub struct ContentSpacer {
+    /// The yew component key.
+    #[prop_or_default]
+    pub key: Option<Key>,
+
+    #[prop_or_default]
+    pub children: Vec<VNode>,
+
+    /// CSS class of the container.
+    #[prop_or_default]
+    pub class: Classes,
+
+    /// CSS style for the dialog window
+    #[prop_or_default]
+    pub styles: CssStyles,
+}
+
+impl ContentSpacer {
+    pub fn new() -> Self {
+        yew::props!(Self {})
+    }
+
+    /// Builder style method to set the yew `key` property.
+    pub fn key(mut self, key: impl IntoOptionalKey) -> Self {
+        self.key = key.into_optional_key();
+        self
+    }
+
+    /// Builder style method to add a html class.
+    pub fn class(mut self, class: impl Into<Classes>) -> Self {
+        self.add_class(class);
+        self
+    }
+
+    /// Method to add a html class.
+    pub fn add_class(&mut self, class: impl Into<Classes>) {
+        self.class.push(class);
+    }
+}
+
+impl ContainerBuilder for ContentSpacer {
+    fn as_children_mut(&mut self) -> &mut Vec<VNode> {
+        &mut self.children
+    }
+}
+
+impl AsClassesMut for ContentSpacer {
+    fn as_classes_mut(&mut self) -> &mut yew::Classes {
+        &mut self.class
+    }
+}
+
+impl AsCssStylesMut for ContentSpacer {
+    fn as_css_styles_mut(&mut self) -> &mut CssStyles {
+        &mut self.styles
+    }
+}
+
+impl WidgetStyleBuilder for ContentSpacer {}
+
+pub struct ProxmoxContentSpacer {}
+
+impl Component for ProxmoxContentSpacer {
+    type Message = ();
+    type Properties = ContentSpacer;
+
+    fn create(_ctx: &Context<Self>) -> Self {
+        Self {}
+    }
+
+    fn view(&self, ctx: &Context<Self>) -> Html {
+        let props = ctx.props();
+        Container::new()
+            .class("proxmox-content-spacer")
+            .class((props.children.len() < 2).then(|| "proxmox-content-spacer-with-one-child"))
+            .class(props.class.clone())
+            .styles(props.styles.clone())
+            .children(props.children.clone())
+            .into()
+    }
+}
+
+impl From<ContentSpacer> for VNode {
+    fn from(val: ContentSpacer) -> Self {
+        let key = val.key.clone();
+        let comp = VComp::new::<ProxmoxContentSpacer>(Rc::new(val), key);
+        VNode::from(comp)
+    }
+}
diff --git a/ui/src/widget/mod.rs b/ui/src/widget/mod.rs
index b885d1b..1104b01 100644
--- a/ui/src/widget/mod.rs
+++ b/ui/src/widget/mod.rs
@@ -1,3 +1,6 @@
+mod content_spacer;
+pub use content_spacer::ContentSpacer;
+
 mod migrate_window;
 pub use migrate_window::MigrateWindow;
 
-- 
2.39.5




More information about the pdm-devel mailing list