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

Dietmar Maurer dietmar at proxmox.com
Fri Jan 10 12:28:44 CET 2025


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

 This only change the "Desktop" Theme layout.

 TODO: change borders to shadows...


 ui/css/crisp-yew-style.scss            | 12 ++++
 ui/css/desktop-yew-style.scss          |  5 ++
 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/main_menu.rs                    | 22 +++---
 ui/src/widget/content_spacer.rs        | 97 ++++++++++++++++++++++++++
 ui/src/widget/mod.rs                   |  3 +
 11 files changed, 223 insertions(+), 35 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..a14a277 100644
--- a/ui/css/desktop-yew-style.scss
+++ b/ui/css/desktop-yew-style.scss
@@ -1,2 +1,7 @@
 @import "../pwt-assets/scss/desktop-yew-style";
 @import "pdm";
+
+.proxmox-content-spacer {
+    padding: var(--pwt-spacer-4);
+    gap: var(--pwt-spacer-4);
+}
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/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/widget/content_spacer.rs b/ui/src/widget/content_spacer.rs
new file mode 100644
index 0000000..37f7e93
--- /dev/null
+++ b/ui/src/widget/content_spacer.rs
@@ -0,0 +1,97 @@
+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
+    }
+}
+
+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