From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager 3/3] ui: restructure wizard to have a better flow
Date: Thu, 19 Dec 2024 13:09:20 +0100 [thread overview]
Message-ID: <20241219120920.2046373-6-d.csapak@proxmox.com> (raw)
In-Reply-To: <20241219120920.2046373-1-d.csapak@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
next prev parent reply other threads:[~2024-12-19 12:09 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-12-19 12:09 [pdm-devel] [PATCH yew-widget-toolkit/proxmox-api-types/pdm] overhaul remote wizard Dominik Csapak
2024-12-19 12:09 ` [pdm-devel] [PATCH yew-widget-toolkit 1/1] widget: input panel: use correct column for custom childs Dominik Csapak
2024-12-19 12:22 ` [pdm-devel] applied: " Thomas Lamprecht
2024-12-19 12:09 ` [pdm-devel] [PATCH proxmox-api-types 1/1] add /access/domains GET call Dominik Csapak
2024-12-19 12:22 ` [pdm-devel] applied: " Thomas Lamprecht
2024-12-19 12:09 ` [pdm-devel] [PATCH datacenter-manager 1/3] server: api: add 'realms' add point for PVE Dominik Csapak
2024-12-19 12:09 ` [pdm-devel] [PATCH datacenter-manager 2/3] pdm-client: expose `ListRealm` type Dominik Csapak
2024-12-19 12:09 ` Dominik Csapak [this message]
2024-12-19 12:31 ` [pdm-devel] applied: [PATCH yew-widget-toolkit/proxmox-api-types/pdm] overhaul remote wizard Thomas Lamprecht
2024-12-19 12:35 ` Dominik Csapak
2024-12-19 12:40 ` Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20241219120920.2046373-6-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal