[pdm-devel] [PATCH datacenter-manager 3/3] ui: restructure wizard to have a better flow

Dominik Csapak d.csapak at proxmox.com
Thu Dec 19 13:09:20 CET 2024


instead of entering all connection info on the first page, just require
the hostname/port (+fingerprint). Then on the second page, require an id
and either user/password/realm/new tokenname or existing token/secret.

the third page is then the endpoint list, and the summary stayed the
same.

This requires a bit of restructuring of the place and way we collect the
information.

Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
without patch 1 for yew-widget-toolkit, the right radio button is not
correctly placed.

 ui/src/remotes/add_wizard.rs          |  37 ++-
 ui/src/remotes/mod.rs                 |   5 +-
 ui/src/remotes/wizard_page_connect.rs | 115 ++++-----
 ui/src/remotes/wizard_page_info.rs    | 336 ++++++++++++++++++++++----
 4 files changed, 363 insertions(+), 130 deletions(-)

diff --git a/ui/src/remotes/add_wizard.rs b/ui/src/remotes/add_wizard.rs
index f8c9bba..f4bf9a3 100644
--- a/ui/src/remotes/add_wizard.rs
+++ b/ui/src/remotes/add_wizard.rs
@@ -13,7 +13,10 @@ use proxmox_yew_comp::{
 };
 use yew::virtual_dom::VNode;
 
-use super::{WizardPageConnect, WizardPageInfo, WizardPageNodes, WizardPageSummary};
+use super::{
+    wizard_page_connect::ConnectParams, WizardPageConnect, WizardPageInfo, WizardPageNodes,
+    WizardPageSummary,
+};
 
 use pwt_macros::builder;
 
@@ -51,9 +54,11 @@ impl AddWizard {
 
 pub enum Msg {
     ServerChange(Option<Remote>),
+    ConnectChange(Option<ConnectParams>),
 }
 pub struct AddWizardState {
     server_info: Option<Remote>,
+    connect_info: Option<ConnectParams>,
 }
 
 impl Component for AddWizardState {
@@ -61,7 +66,10 @@ impl Component for AddWizardState {
     type Properties = AddWizard;
 
     fn create(_ctx: &Context<Self>) -> Self {
-        Self { server_info: None }
+        Self {
+            server_info: None,
+            connect_info: None,
+        }
     }
 
     fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
@@ -69,6 +77,9 @@ impl Component for AddWizardState {
             Msg::ServerChange(server_info) => {
                 self.server_info = server_info;
             }
+            Msg::ConnectChange(realms) => {
+                self.connect_info = realms;
+            }
         }
         true
     }
@@ -93,11 +104,21 @@ impl Component for AddWizardState {
                     let link = ctx.link().clone();
                     move |p: &WizardPageRenderInfo| {
                         WizardPageConnect::new(p.clone(), remote_type)
-                            .on_server_change(link.callback(Msg::ServerChange))
+                            .on_connect_change(link.callback(Msg::ConnectChange))
                             .into()
                     }
                 },
-            );
+            )
+            .with_page(TabBarItem::new().key("info").label(tr!("Settings")), {
+                let realms = self.connect_info.clone();
+                let link = ctx.link().clone();
+                move |p: &WizardPageRenderInfo| {
+                    WizardPageInfo::new(p.clone())
+                        .connect_info(realms.clone())
+                        .on_server_change(link.callback(Msg::ServerChange))
+                        .into()
+                }
+            });
 
         if remote_type == RemoteType::Pve {
             wizard = wizard.with_page(TabBarItem::new().key("nodes").label(tr!("Endpoints")), {
@@ -111,14 +132,6 @@ impl Component for AddWizardState {
         }
 
         wizard
-            .with_page(TabBarItem::new().key("info").label(tr!("Settings")), {
-                let server_info = self.server_info.clone();
-                move |p: &WizardPageRenderInfo| {
-                    WizardPageInfo::new(p.clone())
-                        .server_info(server_info.clone())
-                        .into()
-                }
-            })
             .with_page(TabBarItem::new().label(tr!("Summary")), {
                 let server_info = self.server_info.clone();
                 move |p: &WizardPageRenderInfo| {
diff --git a/ui/src/remotes/mod.rs b/ui/src/remotes/mod.rs
index c9a4e43..cc91b3f 100644
--- a/ui/src/remotes/mod.rs
+++ b/ui/src/remotes/mod.rs
@@ -261,7 +261,10 @@ impl LoadableComponent for PbsRemoteConfigPanel {
                 ConfirmDialog::new()
                     .title(tr!("Confirm: Remove Remote"))
                     .confirm_text(tr!("Remove"))
-                    .confirm_message(tr!("Are you sure you want to remove the remote '{0}' ?", key))
+                    .confirm_message(tr!(
+                        "Are you sure you want to remove the remote '{0}' ?",
+                        key
+                    ))
                     .on_confirm(ctx.link().callback(|_| Msg::RemoveItem))
                     .on_done(ctx.link().change_view_callback(|_| None))
                     .into()
diff --git a/ui/src/remotes/wizard_page_connect.rs b/ui/src/remotes/wizard_page_connect.rs
index ebcae8c..28f14bf 100644
--- a/ui/src/remotes/wizard_page_connect.rs
+++ b/ui/src/remotes/wizard_page_connect.rs
@@ -1,22 +1,21 @@
 use std::rc::Rc;
 
-use anyhow::Error;
+use anyhow::{bail, Error};
 use serde::{Deserialize, Serialize};
+use serde_json::json;
 use yew::html::IntoEventCallback;
 use yew::virtual_dom::{Key, VComp, VNode};
 
 use pwt::css::{AlignItems, FlexFit};
-use pwt::widget::form::{Field, FormContext, FormContextObserver, InputType};
+use pwt::widget::form::{Field, FormContext, FormContextObserver};
 use pwt::widget::{error_message, Button, Column, Container, InputPanel, Mask, Row};
 use pwt::{prelude::*, AsyncPool};
 
 use proxmox_yew_comp::{SchemaValidation, WizardPageRenderInfo};
 
-use proxmox_schema::property_string::PropertyString;
-use proxmox_schema::ApiType;
-
-use pdm_api_types::remotes::{NodeUrl, Remote, RemoteType};
+use pdm_api_types::remotes::RemoteType;
 use pdm_api_types::CERT_FINGERPRINT_SHA256_SCHEMA;
+use pdm_client::types::ListRealm;
 
 use pwt_macros::builder;
 
@@ -25,9 +24,9 @@ use pwt_macros::builder;
 pub struct WizardPageConnect {
     info: WizardPageRenderInfo,
 
-    #[builder_cb(IntoEventCallback, into_event_callback, Option<Remote>)]
+    #[builder_cb(IntoEventCallback, into_event_callback, Option<ConnectParams>)]
     #[prop_or_default]
-    pub on_server_change: Option<Callback<Option<Remote>>>,
+    pub on_connect_change: Option<Callback<Option<ConnectParams>>>,
 
     remote_type: RemoteType,
 }
@@ -38,35 +37,32 @@ impl WizardPageConnect {
     }
 }
 
-async fn scan(connect: ConnectParams) -> Result<Remote, Error> {
-    let params = serde_json::to_value(&connect)?;
-    let mut result: Remote = proxmox_yew_comp::http_post("/pve/scan", Some(params)).await?;
-
-    // insert the initial connection too, since we know that works
-    result.nodes.insert(
-        0,
-        PropertyString::new(NodeUrl {
-            hostname: connect.hostname,
-            fingerprint: connect.fingerprint,
-        }),
-    );
-
-    result.nodes.sort_by(|a, b| a.hostname.cmp(&b.hostname));
+async fn list_realms(
+    hostname: String,
+    fingerprint: Option<String>,
+) -> Result<Vec<ListRealm>, Error> {
+    let mut params = json!({
+        "hostname": hostname,
+    });
+    if let Some(fp) = fingerprint {
+        params["fingerprint"] = fp.into();
+    }
+    let result: Vec<ListRealm> = proxmox_yew_comp::http_post("/pve/realms", Some(params)).await?;
 
     Ok(result)
 }
 
-#[derive(Deserialize, Serialize)]
+#[derive(PartialEq, Clone, Deserialize, Serialize)]
 /// Parameters for connect call.
 pub struct ConnectParams {
-    hostname: String,
-    authid: String,
-    token: String,
+    pub hostname: String,
     #[serde(skip_serializing_if = "Option::is_none")]
-    fingerprint: Option<String>,
+    pub fingerprint: Option<String>,
+    #[serde(default)]
+    pub realms: Vec<ListRealm>,
 }
 
-async fn connect(form_ctx: FormContext, remote_type: RemoteType) -> Result<Remote, Error> {
+async fn connect(form_ctx: FormContext, remote_type: RemoteType) -> Result<ConnectParams, Error> {
     let data = form_ctx.get_submit_data();
     let mut data: ConnectParams = serde_json::from_value(data.clone())?;
     if let Some(hostname) = data.hostname.strip_prefix("http://") {
@@ -79,28 +75,22 @@ async fn connect(form_ctx: FormContext, remote_type: RemoteType) -> Result<Remot
         data.hostname = hostname.to_string();
     }
 
-    Ok(match remote_type {
-        RemoteType::Pve => scan(data).await?,
-        RemoteType::Pbs => Remote {
-            ty: remote_type,
-            id: data.hostname.clone(),
-            authid: data.authid.parse()?,
-            token: data.token,
-            nodes: vec![PropertyString::new(NodeUrl {
-                hostname: data.hostname,
-                fingerprint: data.fingerprint,
-            })],
-        },
-    })
+    let realms = match remote_type {
+        RemoteType::Pve => list_realms(data.hostname.clone(), data.fingerprint.clone()).await?,
+        RemoteType::Pbs => bail!("not implemented"),
+    };
+
+    data.realms = realms;
+    Ok(data)
 }
 
 pub enum Msg {
     FormChange,
     Connect,
-    ConnectResult(Result<Remote, Error>),
+    ConnectResult(Result<ConnectParams, Error>),
 }
 pub struct PdmWizardPageConnect {
-    server_info: Option<Remote>,
+    connect_info: Option<ConnectParams>,
     _form_observer: FormContextObserver,
     form_valid: bool,
     loading: bool,
@@ -109,12 +99,12 @@ pub struct PdmWizardPageConnect {
 }
 
 impl PdmWizardPageConnect {
-    fn update_server_info(&mut self, ctx: &Context<Self>, server_info: Option<Remote>) {
+    fn update_connect_info(&mut self, ctx: &Context<Self>, info: Option<ConnectParams>) {
         let props = ctx.props();
-        self.server_info = server_info;
-        props.info.page_lock(self.server_info.is_none());
-        if let Some(on_server_change) = &props.on_server_change {
-            on_server_change.emit(self.server_info.clone());
+        self.connect_info = info.clone();
+        props.info.page_lock(info.is_none());
+        if let Some(on_connect_change) = &props.on_connect_change {
+            on_connect_change.emit(info);
         }
     }
 }
@@ -133,7 +123,7 @@ impl Component for PdmWizardPageConnect {
         props.info.page_lock(true);
 
         Self {
-            server_info: None,
+            connect_info: None,
             _form_observer,
             form_valid: false,
             loading: false,
@@ -149,7 +139,7 @@ impl Component for PdmWizardPageConnect {
                 self.form_valid = props.info.form_ctx.read().is_valid();
                 match props.remote_type {
                     RemoteType::Pve => {
-                        self.update_server_info(ctx, None);
+                        self.update_connect_info(ctx, None);
                     }
                     RemoteType::Pbs => {
                         return <Self as yew::Component>::update(self, ctx, Msg::Connect)
@@ -158,7 +148,7 @@ impl Component for PdmWizardPageConnect {
             }
             Msg::Connect => {
                 let link = ctx.link().clone();
-                self.update_server_info(ctx, None);
+                self.update_connect_info(ctx, None);
                 let form_ctx = props.info.form_ctx.clone();
                 self.loading = true;
                 self.last_error = None;
@@ -172,8 +162,8 @@ impl Component for PdmWizardPageConnect {
             Msg::ConnectResult(server_info) => {
                 self.loading = false;
                 match server_info {
-                    Ok(server_info) => {
-                        self.update_server_info(ctx, Some(server_info));
+                    Ok(connect_info) => {
+                        self.update_connect_info(ctx, Some(connect_info));
                     }
                     Err(err) => {
                         self.last_error = Some(err);
@@ -196,28 +186,13 @@ impl Component for PdmWizardPageConnect {
             // FIXME: input panel css style is not optimal here...
             .width("auto")
             .padding(4)
-            .with_field(
+            .with_large_field(
                 tr!("Server Address"),
                 Field::new()
                     .name("hostname")
                     .placeholder(tr!("<IP/Hostname>:Port"))
                     .required(true),
             )
-            .with_right_field(
-                tr!("User/Token"),
-                Field::new()
-                    .name("authid")
-                    .placeholder(tr!("Example: user at pve!tokenid"))
-                    .schema(&pdm_api_types::Authid::API_SCHEMA)
-                    .required(true),
-            )
-            .with_right_field(
-                tr!("Password/Secret"),
-                Field::new()
-                    .name("token")
-                    .input_type(InputType::Password)
-                    .required(true),
-            )
             .with_large_field(
                 tr!("Fingerprint"),
                 Field::new()
@@ -242,7 +217,7 @@ impl Component for PdmWizardPageConnect {
                         )
                         .with_flex_spacer()
                         .with_optional_child(
-                            (self.last_error.is_none() && self.server_info.is_some())
+                            (self.last_error.is_none() && self.connect_info.is_some())
                                 .then_some(Container::new().with_child(tr!("Connection OK"))),
                         )
                         .with_child(
diff --git a/ui/src/remotes/wizard_page_info.rs b/ui/src/remotes/wizard_page_info.rs
index 7874eae..c67be78 100644
--- a/ui/src/remotes/wizard_page_info.rs
+++ b/ui/src/remotes/wizard_page_info.rs
@@ -1,30 +1,41 @@
 use std::rc::Rc;
 
-use yew::virtual_dom::{VComp, VNode};
+use anyhow::Error;
+use html::IntoEventCallback;
+use proxmox_schema::property_string::PropertyString;
+use serde::{Deserialize, Serialize};
+use yew::virtual_dom::{Key, VComp, VNode};
 
+use proxmox_yew_comp::WizardPageRenderInfo;
 use pwt::{
-    css::FlexFit,
+    css::{self, FlexFit},
     prelude::*,
     widget::{
-        form::{Checkbox, Field},
-        InputPanel,
+        error_message,
+        form::{Combobox, Field, FormContext, FormContextObserver, InputType, RadioButton},
+        Button, Column, Container, InputPanel, Mask, Row,
     },
+    AsyncPool,
 };
 
-use proxmox_yew_comp::WizardPageRenderInfo;
-
-use pdm_api_types::{remotes::Remote, Authid};
+use pdm_api_types::remotes::{NodeUrl, Remote};
 
 use pwt_macros::builder;
 
+use super::wizard_page_connect::ConnectParams;
+
 #[derive(Clone, PartialEq, Properties)]
 #[builder]
 pub struct WizardPageInfo {
     info: WizardPageRenderInfo,
 
+    #[builder_cb(IntoEventCallback, into_event_callback, Option<Remote>)]
+    #[prop_or_default]
+    pub on_server_change: Option<Callback<Option<Remote>>>,
+
     #[builder]
     #[prop_or_default]
-    server_info: Option<Remote>,
+    connect_info: Option<ConnectParams>,
 }
 
 impl WizardPageInfo {
@@ -34,11 +45,102 @@ impl WizardPageInfo {
 }
 
 pub struct PdmWizardPageInfo {
-    create_token: bool,
+    user_mode: bool,
+    realms: Rc<Vec<AttrValue>>,
+    server_info: Option<Remote>,
+    last_error: Option<Error>,
+    credentials: Option<(String, String)>,
+    loading: bool,
+    _form_observer: FormContextObserver,
+    async_pool: AsyncPool,
 }
 
 pub enum Msg {
     ToggleCreateToken(bool),
+    FormChange,
+    Connect,
+    ConnectResult(Result<Remote, Error>),
+}
+
+#[derive(Deserialize, Serialize)]
+/// Parameters for connect call.
+pub struct ScanParams {
+    hostname: String,
+    authid: String,
+    token: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    fingerprint: Option<String>,
+}
+
+fn create_realm_list(props: &WizardPageInfo) -> Rc<Vec<AttrValue>> {
+    if let Some(info) = &props.connect_info {
+        let realms = Rc::new(
+            info.realms
+                .iter()
+                .map(|realm| AttrValue::from(realm.realm.clone()))
+                .collect(),
+        );
+        realms
+    } else {
+        Rc::new(Vec::new())
+    }
+}
+
+async fn scan(connection_params: ConnectParams, form_ctx: FormContext) -> Result<Remote, Error> {
+    let mut data = form_ctx.get_submit_data();
+
+    data["hostname"] = connection_params.hostname.into();
+    if let Some(fp) = connection_params.fingerprint {
+        data["fingerprint"] = fp.into();
+    }
+
+    let data: ScanParams = serde_json::from_value(data.clone())?;
+
+    let params = serde_json::to_value(&data)?;
+    let mut result: Remote = proxmox_yew_comp::http_post("/pve/scan", Some(params)).await?;
+    result.nodes.insert(
+        0,
+        PropertyString::new(NodeUrl {
+            hostname: data.hostname,
+            fingerprint: data.fingerprint,
+        }),
+    );
+    result.nodes.sort_by(|a, b| a.hostname.cmp(&b.hostname));
+    Ok(result)
+}
+
+impl PdmWizardPageInfo {
+    fn update_credentials(form_ctx: &FormContext) {
+        let user = form_ctx.read().get_field_text("user");
+        let realm = form_ctx.read().get_field_text("realm");
+        let password = form_ctx.read().get_field_text("password");
+
+        let user_mode = form_ctx.read().get_field_text("login-mode") == "login";
+
+        let tokenid = form_ctx.read().get_field_text("tokenid");
+        let secret = form_ctx.read().get_field_text("secret");
+
+        let (authid, token) =
+            if user_mode && !user.is_empty() && !realm.is_empty() && !password.is_empty() {
+                (format!("{user}@{realm}").into(), password.into())
+            } else if !user_mode && !tokenid.is_empty() && !secret.is_empty() {
+                (tokenid.into(), secret.into())
+            } else {
+                (serde_json::Value::Null, serde_json::Value::Null)
+            };
+
+        form_ctx.write().set_field_value("authid", authid);
+        form_ctx.write().set_field_value("token", token);
+    }
+
+    fn update_server_info(&mut self, ctx: &Context<Self>, server_info: Option<Remote>) {
+        let props = ctx.props();
+        self.server_info = server_info;
+        props.info.page_lock(self.server_info.is_none());
+        if let Some(on_server_change) = &props.on_server_change {
+            on_server_change.emit(self.server_info.clone());
+        }
+    }
 }
 
 impl Component for PdmWizardPageInfo {
@@ -47,67 +149,207 @@ impl Component for PdmWizardPageInfo {
 
     fn create(ctx: &Context<Self>) -> Self {
         let props = ctx.props();
-        if props.server_info.is_none() {
-            props.info.page_lock(true);
-        }
 
-        Self { create_token: true }
+        props.info.page_lock(true);
+
+        let _form_observer = props
+            .info
+            .form_ctx
+            .add_listener(ctx.link().callback(|_| Msg::FormChange));
+
+        Self {
+            server_info: None,
+            user_mode: true,
+            realms: create_realm_list(props),
+            _form_observer,
+            last_error: None,
+            loading: false,
+            credentials: None,
+            async_pool: AsyncPool::new(),
+        }
     }
 
-    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
+        let props = ctx.props();
         match msg {
             Msg::ToggleCreateToken(create_token) => {
-                self.create_token = create_token;
+                self.user_mode = create_token;
+            }
+            Msg::FormChange => {
+                let form_ctx = &props.info.form_ctx;
+                Self::update_credentials(form_ctx);
+                let authid = form_ctx.read().get_field_text("authid");
+                let token = form_ctx.read().get_field_text("token");
+                if !authid.is_empty() && !token.is_empty() {
+                    match &self.credentials {
+                        Some((old_auth, old_token))
+                            if *old_auth == authid && *old_token == token => {}
+                        Some(_) | None => {
+                            self.credentials = Some((authid, token));
+                            self.update_server_info(ctx, None);
+                        }
+                    }
+                } else {
+                    self.credentials = None;
+                }
+            }
+            Msg::Connect => {
+                let link = ctx.link().clone();
+                self.update_server_info(ctx, None);
+                let form_ctx = props.info.form_ctx.clone();
+                self.loading = true;
+                self.last_error = None;
+
+                if let Some(connection_info) = props.connect_info.clone() {
+                    self.async_pool.spawn(async move {
+                        let result = scan(connection_info, form_ctx).await;
+                        link.send_message(Msg::ConnectResult(result));
+                    });
+                } else {
+                    unreachable!("Settings page must have connection info");
+                }
+            }
+            Msg::ConnectResult(server_info) => {
+                self.loading = false;
+                match server_info {
+                    Ok(server_info) => {
+                        self.update_server_info(ctx, Some(server_info));
+                    }
+                    Err(err) => {
+                        self.last_error = Some(err);
+                    }
+                }
+
+                if let Some(form_ctx) = props.info.lookup_form_context(&Key::from("nodes")) {
+                    form_ctx.write().reset_form();
+                }
+                props.info.reset_remaining_valid_pages();
             }
         }
         true
     }
 
+    fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
+        self.realms = create_realm_list(ctx.props());
+        true
+    }
+
     fn view(&self, ctx: &Context<Self>) -> Html {
-        let mut is_user = true;
-        if let Some(Some(authid)) = ctx
-            .props()
-            .info
-            .valid_data
-            .get("authid")
-            .map(|a| a.as_str())
-        {
-            match authid.parse::<Authid>() {
-                Ok(authid) => is_user = !authid.is_token(),
-                Err(_) => {}
-            }
-        }
-        let name = ctx
-            .props()
-            .server_info
-            .as_ref()
-            .map(|s| s.id.to_string())
-            .unwrap_or_default();
-        InputPanel::new()
+        let input_panel = InputPanel::new()
             .class(FlexFit)
             .padding(4)
+            .with_field(tr!("Remote ID"), Field::new().name("id").required(true))
+            .with_custom_child(
+                RadioButton::new("login")
+                    .key("login-mode-login")
+                    .name("login-mode")
+                    .default(true)
+                    .box_label(tr!("Login and create Token"))
+                    .on_change(
+                        ctx.link()
+                            .callback(|value| Msg::ToggleCreateToken(value == "login")),
+                    ),
+            )
+            .with_field(
+                tr!("User"),
+                Field::new()
+                    .name("user")
+                    .disabled(!self.user_mode)
+                    .required(self.user_mode)
+                    .submit(false),
+            )
             .with_field(
-                tr!("Remote ID"),
-                Field::new().default(name).name("id").required(true),
+                tr!("Password"),
+                Field::new()
+                    .input_type(InputType::Password)
+                    .name("password")
+                    .disabled(!self.user_mode)
+                    .required(self.user_mode)
+                    .submit(false),
             )
             .with_field(
-                tr!("Create API Token"),
-                Checkbox::new()
-                    .key("create-token-cb")
-                    .submit(false)
-                    .disabled(is_user)
-                    .default(self.create_token || is_user)
-                    .on_change(ctx.link().callback(Msg::ToggleCreateToken)),
+                tr!("Realm"),
+                Combobox::new()
+                    .name("realm")
+                    .disabled(!self.user_mode)
+                    .required(self.user_mode)
+                    .items(self.realms.clone())
+                    .submit(false),
             )
             .with_field(
                 tr!("API Token Name"),
                 Field::new()
                     .name("create-token")
-                    .disabled(!self.create_token && !is_user)
-                    .required(self.create_token || is_user)
-                    .submit(self.create_token || is_user)
+                    .disabled(!self.user_mode)
+                    .required(self.user_mode)
+                    .submit(self.user_mode)
                     .default("pdm-admin"),
             )
+            .with_right_custom_child(Container::new().key("spacer")) //spacer
+            .with_right_custom_child(
+                RadioButton::new("token")
+                    .key("login-mode-token")
+                    .name("login-mode")
+                    .box_label(tr!("Use existing Token")),
+            )
+            .with_right_field(
+                tr!("Token"),
+                Field::new()
+                    .name("tokenid")
+                    .disabled(self.user_mode)
+                    .required(!self.user_mode)
+                    .submit(false),
+            )
+            .with_right_field(
+                tr!("Secret"),
+                Field::new()
+                    .name("secret")
+                    .input_type(InputType::Password)
+                    .disabled(self.user_mode)
+                    .required(!self.user_mode)
+                    .submit(false),
+            )
+            .with_field_and_options(
+                pwt::widget::FieldPosition::Left,
+                false,
+                true,
+                tr!(""),
+                Field::new().name("token").required(true),
+            )
+            .with_field_and_options(
+                pwt::widget::FieldPosition::Left,
+                false,
+                true,
+                tr!(""),
+                Field::new().name("authid").required(true),
+            );
+        let content = Column::new()
+            .class(FlexFit)
+            .with_child(input_panel)
+            .with_child(
+                Row::new()
+                    .padding(2)
+                    .gap(2)
+                    .class(css::AlignItems::Center)
+                    .with_optional_child(
+                        self.last_error
+                            .as_deref()
+                            .map(|err| error_message(&err.to_string())),
+                    )
+                    .with_flex_spacer()
+                    .with_optional_child(
+                        (self.last_error.is_none() && self.server_info.is_some())
+                            .then_some(Container::new().with_child(tr!("Scan OK"))),
+                    )
+                    .with_child(
+                        Button::new("Scan")
+                            .disabled(self.credentials.is_none())
+                            .onclick(ctx.link().callback(|_| Msg::Connect)),
+                    ),
+            );
+        Mask::new(content)
+            .class(FlexFit)
+            .visible(self.loading)
             .into()
     }
 }
-- 
2.39.5





More information about the pdm-devel mailing list