From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: <pdm-devel-bounces@lists.proxmox.com> Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 8A0E31FF163 for <inbox@lore.proxmox.com>; Thu, 19 Dec 2024 13:09:27 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id E80AC45FB; Thu, 19 Dec 2024 13:09:27 +0100 (CET) From: Dominik Csapak <d.csapak@proxmox.com> To: pdm-devel@lists.proxmox.com Date: Thu, 19 Dec 2024 13:09:20 +0100 Message-Id: <20241219120920.2046373-6-d.csapak@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20241219120920.2046373-1-d.csapak@proxmox.com> References: <20241219120920.2046373-1-d.csapak@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.184 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_2 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_4 0.1 random spam to be learned in bayes PROLO_LEO1 0.1 Meta Catches all Leo drug variations so far SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pdm-devel] [PATCH datacenter-manager 3/3] ui: restructure wizard to have a better flow X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion <pdm-devel.lists.proxmox.com> List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pdm-devel>, <mailto:pdm-devel-request@lists.proxmox.com?subject=unsubscribe> List-Archive: <http://lists.proxmox.com/pipermail/pdm-devel/> List-Post: <mailto:pdm-devel@lists.proxmox.com> List-Help: <mailto:pdm-devel-request@lists.proxmox.com?subject=help> List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel>, <mailto:pdm-devel-request@lists.proxmox.com?subject=subscribe> Reply-To: Proxmox Datacenter Manager development discussion <pdm-devel@lists.proxmox.com> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" <pdm-devel-bounces@lists.proxmox.com> 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@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@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 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel