From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id B9EBA1FF16E for ; Mon, 20 Jan 2025 10:30:48 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id B391616349; Mon, 20 Jan 2025 10:30:47 +0100 (CET) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Date: Mon, 20 Jan 2025 10:30:03 +0100 Message-Id: <20250120093006.927014-14-d.csapak@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250120093006.927014-1-d.csapak@proxmox.com> References: <20250120093006.927014-1-d.csapak@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.131 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 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 6/9] ui: refactor RemoteConfigPanel into own module X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" we'll use the 'remote' module for a more general component where we use the RemoteConfigPanel Signed-off-by: Dominik Csapak --- ui/src/remotes/config.rs | 392 +++++++++++++++++++++++++++++++++++++++ ui/src/remotes/mod.rs | 392 +-------------------------------------- 2 files changed, 394 insertions(+), 390 deletions(-) create mode 100644 ui/src/remotes/config.rs diff --git a/ui/src/remotes/config.rs b/ui/src/remotes/config.rs new file mode 100644 index 0000000..d9d16ae --- /dev/null +++ b/ui/src/remotes/config.rs @@ -0,0 +1,392 @@ +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; + +use anyhow::Error; + +use proxmox_schema::property_string::PropertyString; + +use crate::remotes::edit_remote::EditRemote; +//use pwt::widget::form::{Field, FormContext, InputType}; + +use pdm_api_types::remotes::Remote; +//use proxmox_schema::{property_string::PropertyString, ApiType}; +use proxmox_yew_comp::percent_encoding::percent_encode_component; + +//use pbs_api_types::CERT_FINGERPRINT_SHA256_SCHEMA; + +//use proxmox_schema::api_types::{CERT_FINGERPRINT_SHA256_SCHEMA, DNS_NAME_OR_IP_SCHEMA}; + +use serde_json::Value; +use yew::virtual_dom::{Key, VComp, VNode}; + +use pwt::prelude::*; +use pwt::state::{Selection, Store}; +use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader}; +//use pwt::widget::form::{delete_empty_values, Field, FormContext, InputType}; +use pwt::widget::{ + //menu::{Menu, MenuButton, MenuItem}, + Button, + Column, + Toolbar, + Tooltip, +}; +//use pwt::widget::InputPanel; + +//use proxmox_yew_comp::EditWindow; +use proxmox_yew_comp::{ + ConfirmButton, LoadableComponent, LoadableComponentContext, LoadableComponentMaster, +}; + +use pdm_api_types::remotes::{NodeUrl, RemoteType}; + +async fn load_remotes() -> Result, Error> { + proxmox_yew_comp::http_get("/remotes", None).await +} + +async fn delete_item(key: Key) -> Result<(), Error> { + let id = key.to_string(); + let url = format!("/remotes/{}", percent_encode_component(&id)); + proxmox_yew_comp::http_delete(&url, None).await?; + Ok(()) +} + +pub async fn create_remote(mut data: Value, remote_type: RemoteType) -> Result<(), Error> { + if data.get("nodes").is_none() { + let nodes = vec![PropertyString::new(NodeUrl { + hostname: data["hostname"].as_str().unwrap_or_default().to_string(), + fingerprint: data["fingerprint"].as_str().map(|fp| fp.to_string()), + })]; + data["nodes"] = serde_json::to_value(nodes)?; + } + data["type"] = match remote_type { + RemoteType::Pve => "pve", + RemoteType::Pbs => "pbs", + } + .into(); + + let remote: Remote = serde_json::from_value(data.clone())?; + + let mut params = serde_json::to_value(remote)?; + if let Some(token) = data["create-token"].as_str() { + params["create-token"] = token.into(); + } + + proxmox_yew_comp::http_post("/remotes", Some(params)).await +} + +/* +async fn update_item(form_ctx: FormContext) -> Result<(), Error> { + let data = form_ctx.get_submit_data(); + + let data = delete_empty_values(&data, &["fingerprint", "comment", "port"], true); + + let name = form_ctx.read().get_field_text("name"); + + let url = format!("/config/remote/{}", percent_encode_component(&name)); + + proxmox_yew_comp::http_put(&url, Some(data)).await +} +*/ + +#[derive(PartialEq, Properties)] +pub struct RemoteConfigPanel; + +impl RemoteConfigPanel { + pub fn new() -> Self { + yew::props!(Self {}) + } +} + +#[derive(PartialEq)] +pub enum ViewState { + Add(RemoteType), + Edit, +} + +pub enum Msg { + SelectionChange, + RemoveItem, +} + +pub struct PbsRemoteConfigPanel { + store: Store, + selection: Selection, + remote_list_columns: Rc>>, +} + +impl LoadableComponent for PbsRemoteConfigPanel { + type Message = Msg; + type Properties = RemoteConfigPanel; + type ViewState = ViewState; + + fn load( + &self, + _ctx: &LoadableComponentContext, + ) -> Pin>>> { + let store = self.store.clone(); + Box::pin(async move { + let data = load_remotes().await?; + store.write().set_data(data); + Ok(()) + }) + } + + fn create(ctx: &LoadableComponentContext) -> Self { + let store = Store::with_extract_key(|record: &Remote| Key::from(record.id.clone())); + + let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::SelectionChange)); + + let remote_list_columns = remote_list_columns(); + + Self { + store, + selection, + remote_list_columns, + } + } + + fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { + match msg { + Msg::SelectionChange => true, + Msg::RemoveItem => { + if let Some(key) = self.selection.selected_key() { + let link = ctx.link(); + link.clone().spawn(async move { + if let Err(err) = delete_item(key).await { + link.show_error(tr!("Unable to delete item"), err, true); + } + link.send_reload(); + }) + } + false + } + } + } + + fn toolbar(&self, ctx: &LoadableComponentContext) -> Option { + let link = ctx.link(); + + let disabled = self.selection.is_empty(); + + let toolbar = Toolbar::new() + .class("pwt-overflow-hidden") + .class("pwt-border-bottom") + .with_child({ + Button::new(tr!("Add Proxmox VE")) + .icon_class("fa fa-building") + .onclick(link.change_view_callback(|_| Some(ViewState::Add(RemoteType::Pve)))) + // FIXME: add PBS support + //MenuButton::new(tr!("Add")).show_arrow(true).menu( + // Menu::new() + // .with_item( + // MenuItem::new("Proxmox VE") + // .icon_class("fa fa-building") + // .on_select(link.change_view_callback(|_| { + // Some(ViewState::Add(RemoteType::Pve)) + // })), + // ) + // .with_item( + // MenuItem::new("Proxmox Backup Server") + // .icon_class("fa fa-floppy-o") + // .on_select(link.change_view_callback(|_| { + // Some(ViewState::Add(RemoteType::Pbs)) + // })), + // ), + //) + }) + .with_spacer() + .with_child( + Button::new(tr!("Edit")) + .disabled(disabled) + .onclick(link.change_view_callback(|_| Some(ViewState::Edit))), + ) + .with_child( + ConfirmButton::new(tr!("Remove")) + .confirm_message(tr!("Are you sure you want to remove this remote?")) + .disabled(disabled) + .on_activate(link.callback(|_| Msg::RemoveItem)), + ) + .with_flex_spacer() + .with_child({ + let loading = ctx.loading(); + let link = ctx.link(); + Button::refresh(loading).onclick(move |_| link.send_reload()) + }); + + Some(toolbar.into()) + } + + fn main_view(&self, ctx: &LoadableComponentContext) -> Html { + let columns = Rc::clone(&self.remote_list_columns); + let link = ctx.link(); + DataTable::new(columns, self.store.clone()) + .class(pwt::css::FlexFit) + .selection(self.selection.clone()) + .on_row_dblclick(move |_: &mut _| { + link.change_view(Some(ViewState::Edit)); + }) + .into() + } + + fn dialog_view( + &self, + ctx: &LoadableComponentContext, + view_state: &Self::ViewState, + ) -> Option { + match view_state { + ViewState::Add(ty) => Some(self.create_add_dialog(ctx, *ty)), + ViewState::Edit => self + .selection + .selected_key() + .map(|key| self.create_edit_dialog(ctx, key)), + } + } +} + +/* +fn add_remote_input_panel(_form_ctx: &FormContext) -> Html { + InputPanel::new() + .padding(4) + .with_field(tr!("Remote ID"), Field::new().name("id").required(true)) + .with_right_field( + tr!("Fingerprint"), + Field::new() + .name("fingerprint") + .schema(&CERT_FINGERPRINT_SHA256_SCHEMA), + ) + .with_field( + tr!("Server address"), + Field::new().name("server").required(true), + ) + .with_field( + tr!("User/Token"), + Field::new() + .name("authid") + .schema(&pdm_api_types::Authid::API_SCHEMA) + .required(true), + ) + .with_field( + tr!("Password/Secret"), + Field::new() + .name("token") + .input_type(InputType::Password) + .required(true), + ) + .into() +} +*/ + +impl PbsRemoteConfigPanel { + fn create_add_dialog( + &self, + ctx: &LoadableComponentContext, + remote_type: RemoteType, + ) -> Html { + super::AddWizard::new(remote_type) + .on_close(ctx.link().change_view_callback(|_| None)) + .on_submit(move |ctx| create_remote(ctx, remote_type)) + .into() + + // EditWindow::new(tr!("Add") + ": " + &tr!("Remote")) + // .renderer(add_remote_input_panel) + // .on_submit(move |ctx: FormContext| create_item(ctx.get_submit_data(), remote_type)) + // .on_done(ctx.link().change_view_callback(|_| None)) + // .into() + } + + fn create_edit_dialog(&self, ctx: &LoadableComponentContext, key: Key) -> Html { + EditRemote::new(&*key) + .on_done(ctx.link().change_view_callback(|_| None)) + .into() + } +} + +impl Into for RemoteConfigPanel { + fn into(self) -> VNode { + let comp = VComp::new::>(Rc::new(self), None); + VNode::from(comp) + } +} + +fn remote_list_columns() -> Rc>> { + Rc::new(vec![ + DataTableColumn::new(tr!("Remote ID")) + .width("200px") + .render(|item: &Remote| { + html! { + &item.id + } + }) + .sorter(|a: &Remote, b: &Remote| a.id.cmp(&b.id)) + .sort_order(true) + .into(), + DataTableColumn::new(tr!("Type")) + .width("60px") + .render(|item: &Remote| { + html! { + &item.ty + } + }) + .sorter(|a: &Remote, b: &Remote| a.ty.cmp(&b.ty)) + .into(), + DataTableColumn::new(tr!("AuthId")) + .width("200px") + .render(|item: &Remote| { + html! { + &item.authid + } + }) + .sorter(|a: &Remote, b: &Remote| a.authid.cmp(&b.authid)) + .into(), + DataTableColumn::new(tr!("Nodes")) + .flex(1) + .render(|item: &Remote| { + if item.nodes.is_empty() { + html! {tr!("None")} + } else { + let nodes = item + .nodes + .iter() + .map(|n| n.hostname.as_str()) + .collect::>() + .join(", "); + let mut tip = Column::new(); + tip.add_children(item.nodes.iter().map(|n| { + let text = match n.fingerprint.clone() { + Some(fp) => format!("{} ({fp})", n.hostname), + None => n.hostname.to_string(), + }; + html! {
{text}
} + })); + Tooltip::new(nodes).rich_tip(tip).into() + } + }) + .into(), + /* + DataTableColumn::new(tr!("Auth ID")) + .width("200px") + .render(|item: &Remote| html!{ + item.config.auth_id.clone() + }) + .sorter(|a: &Remote, b: &Remote| { + a.config.auth_id.cmp(&b.config.auth_id) + }) + .into(), + + DataTableColumn::new(tr!("Fingerprint")) + .width("200px") + .render(|item: &Remote| html!{ + item.config.fingerprint.clone().unwrap_or(String::new()) + }) + .into(), + + DataTableColumn::new(tr!("Comment")) + .flex(1) + .render(|item: &Remote| html!{ + item.config.comment.clone().unwrap_or(String::new()) + }) + .into() + */ + ]) +} diff --git a/ui/src/remotes/mod.rs b/ui/src/remotes/mod.rs index d11c5e0..f221777 100644 --- a/ui/src/remotes/mod.rs +++ b/ui/src/remotes/mod.rs @@ -1,5 +1,4 @@ mod wizard_page_connect; -use proxmox_schema::property_string::PropertyString; use wizard_page_connect::WizardPageConnect; mod wizard_page_nodes; @@ -19,392 +18,5 @@ pub use node_url_list::NodeUrlList; mod edit_remote; -use std::future::Future; -use std::pin::Pin; -use std::rc::Rc; - -use anyhow::Error; -use edit_remote::EditRemote; -//use pwt::widget::form::{Field, FormContext, InputType}; - -use pdm_api_types::remotes::Remote; -//use proxmox_schema::{property_string::PropertyString, ApiType}; -use proxmox_yew_comp::percent_encoding::percent_encode_component; - -//use pbs_api_types::CERT_FINGERPRINT_SHA256_SCHEMA; - -//use proxmox_schema::api_types::{CERT_FINGERPRINT_SHA256_SCHEMA, DNS_NAME_OR_IP_SCHEMA}; - -use serde_json::Value; -use yew::virtual_dom::{Key, VComp, VNode}; - -use pwt::prelude::*; -use pwt::state::{Selection, Store}; -use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader}; -//use pwt::widget::form::{delete_empty_values, Field, FormContext, InputType}; -use pwt::widget::{ - //menu::{Menu, MenuButton, MenuItem}, - Button, - Column, - Toolbar, - Tooltip, -}; -//use pwt::widget::InputPanel; - -//use proxmox_yew_comp::EditWindow; -use proxmox_yew_comp::{ - ConfirmButton, LoadableComponent, LoadableComponentContext, LoadableComponentMaster, -}; - -use pdm_api_types::remotes::{NodeUrl, RemoteType}; - -async fn load_remotes() -> Result, Error> { - proxmox_yew_comp::http_get("/remotes", None).await -} - -async fn delete_item(key: Key) -> Result<(), Error> { - let id = key.to_string(); - let url = format!("/remotes/{}", percent_encode_component(&id)); - proxmox_yew_comp::http_delete(&url, None).await?; - Ok(()) -} - -pub(crate) async fn create_remote(mut data: Value, remote_type: RemoteType) -> Result<(), Error> { - if data.get("nodes").is_none() { - let nodes = vec![PropertyString::new(NodeUrl { - hostname: data["hostname"].as_str().unwrap_or_default().to_string(), - fingerprint: data["fingerprint"].as_str().map(|fp| fp.to_string()), - })]; - data["nodes"] = serde_json::to_value(nodes)?; - } - data["type"] = match remote_type { - RemoteType::Pve => "pve", - RemoteType::Pbs => "pbs", - } - .into(); - - let remote: Remote = serde_json::from_value(data.clone())?; - - let mut params = serde_json::to_value(remote)?; - if let Some(token) = data["create-token"].as_str() { - params["create-token"] = token.into(); - } - - proxmox_yew_comp::http_post("/remotes", Some(params)).await -} - -/* -async fn update_item(form_ctx: FormContext) -> Result<(), Error> { - let data = form_ctx.get_submit_data(); - - let data = delete_empty_values(&data, &["fingerprint", "comment", "port"], true); - - let name = form_ctx.read().get_field_text("name"); - - let url = format!("/config/remote/{}", percent_encode_component(&name)); - - proxmox_yew_comp::http_put(&url, Some(data)).await -} -*/ - -#[derive(PartialEq, Properties)] -pub struct RemoteConfigPanel; - -impl RemoteConfigPanel { - pub fn new() -> Self { - yew::props!(Self {}) - } -} - -#[derive(PartialEq)] -pub enum ViewState { - Add(RemoteType), - Edit, -} - -pub enum Msg { - SelectionChange, - RemoveItem, -} - -pub struct PbsRemoteConfigPanel { - store: Store, - selection: Selection, - remote_list_columns: Rc>>, -} - -impl LoadableComponent for PbsRemoteConfigPanel { - type Message = Msg; - type Properties = RemoteConfigPanel; - type ViewState = ViewState; - - fn load( - &self, - _ctx: &LoadableComponentContext, - ) -> Pin>>> { - let store = self.store.clone(); - Box::pin(async move { - let data = load_remotes().await?; - store.write().set_data(data); - Ok(()) - }) - } - - fn create(ctx: &LoadableComponentContext) -> Self { - let store = Store::with_extract_key(|record: &Remote| Key::from(record.id.clone())); - - let selection = Selection::new().on_select(ctx.link().callback(|_| Msg::SelectionChange)); - - let remote_list_columns = remote_list_columns(); - - Self { - store, - selection, - remote_list_columns, - } - } - - fn update(&mut self, ctx: &LoadableComponentContext, msg: Self::Message) -> bool { - match msg { - Msg::SelectionChange => true, - Msg::RemoveItem => { - if let Some(key) = self.selection.selected_key() { - let link = ctx.link(); - link.clone().spawn(async move { - if let Err(err) = delete_item(key).await { - link.show_error(tr!("Unable to delete item"), err, true); - } - link.send_reload(); - }) - } - false - } - } - } - - fn toolbar(&self, ctx: &LoadableComponentContext) -> Option { - let link = ctx.link(); - - let disabled = self.selection.is_empty(); - - let toolbar = Toolbar::new() - .class("pwt-overflow-hidden") - .class("pwt-border-bottom") - .with_child({ - Button::new(tr!("Add Proxmox VE")) - .icon_class("fa fa-building") - .onclick(link.change_view_callback(|_| Some(ViewState::Add(RemoteType::Pve)))) - // FIXME: add PBS support - //MenuButton::new(tr!("Add")).show_arrow(true).menu( - // Menu::new() - // .with_item( - // MenuItem::new("Proxmox VE") - // .icon_class("fa fa-building") - // .on_select(link.change_view_callback(|_| { - // Some(ViewState::Add(RemoteType::Pve)) - // })), - // ) - // .with_item( - // MenuItem::new("Proxmox Backup Server") - // .icon_class("fa fa-floppy-o") - // .on_select(link.change_view_callback(|_| { - // Some(ViewState::Add(RemoteType::Pbs)) - // })), - // ), - //) - }) - .with_spacer() - .with_child( - Button::new(tr!("Edit")) - .disabled(disabled) - .onclick(link.change_view_callback(|_| Some(ViewState::Edit))), - ) - .with_child( - ConfirmButton::new(tr!("Remove")) - .confirm_message(tr!("Are you sure you want to remove this remote?")) - .disabled(disabled) - .on_activate(link.callback(|_| Msg::RemoveItem)), - ) - .with_flex_spacer() - .with_child({ - let loading = ctx.loading(); - let link = ctx.link(); - Button::refresh(loading).onclick(move |_| link.send_reload()) - }); - - Some(toolbar.into()) - } - - fn main_view(&self, ctx: &LoadableComponentContext) -> Html { - let columns = Rc::clone(&self.remote_list_columns); - let link = ctx.link(); - DataTable::new(columns, self.store.clone()) - .class(pwt::css::FlexFit) - .selection(self.selection.clone()) - .on_row_dblclick(move |_: &mut _| { - link.change_view(Some(ViewState::Edit)); - }) - .into() - } - - fn dialog_view( - &self, - ctx: &LoadableComponentContext, - view_state: &Self::ViewState, - ) -> Option { - match view_state { - ViewState::Add(ty) => Some(self.create_add_dialog(ctx, *ty)), - ViewState::Edit => self - .selection - .selected_key() - .map(|key| self.create_edit_dialog(ctx, key)), - } - } -} - -/* -fn add_remote_input_panel(_form_ctx: &FormContext) -> Html { - InputPanel::new() - .padding(4) - .with_field(tr!("Remote ID"), Field::new().name("id").required(true)) - .with_right_field( - tr!("Fingerprint"), - Field::new() - .name("fingerprint") - .schema(&CERT_FINGERPRINT_SHA256_SCHEMA), - ) - .with_field( - tr!("Server address"), - Field::new().name("server").required(true), - ) - .with_field( - tr!("User/Token"), - Field::new() - .name("authid") - .schema(&pdm_api_types::Authid::API_SCHEMA) - .required(true), - ) - .with_field( - tr!("Password/Secret"), - Field::new() - .name("token") - .input_type(InputType::Password) - .required(true), - ) - .into() -} -*/ - -impl PbsRemoteConfigPanel { - fn create_add_dialog( - &self, - ctx: &LoadableComponentContext, - remote_type: RemoteType, - ) -> Html { - AddWizard::new(remote_type) - .on_close(ctx.link().change_view_callback(|_| None)) - .on_submit(move |ctx| create_remote(ctx, remote_type)) - .into() - - // EditWindow::new(tr!("Add") + ": " + &tr!("Remote")) - // .renderer(add_remote_input_panel) - // .on_submit(move |ctx: FormContext| create_item(ctx.get_submit_data(), remote_type)) - // .on_done(ctx.link().change_view_callback(|_| None)) - // .into() - } - - fn create_edit_dialog(&self, ctx: &LoadableComponentContext, key: Key) -> Html { - EditRemote::new(&*key) - .on_done(ctx.link().change_view_callback(|_| None)) - .into() - } -} - -impl Into for RemoteConfigPanel { - fn into(self) -> VNode { - let comp = VComp::new::>(Rc::new(self), None); - VNode::from(comp) - } -} - -fn remote_list_columns() -> Rc>> { - Rc::new(vec![ - DataTableColumn::new(tr!("Remote ID")) - .width("200px") - .render(|item: &Remote| { - html! { - &item.id - } - }) - .sorter(|a: &Remote, b: &Remote| a.id.cmp(&b.id)) - .sort_order(true) - .into(), - DataTableColumn::new(tr!("Type")) - .width("60px") - .render(|item: &Remote| { - html! { - &item.ty - } - }) - .sorter(|a: &Remote, b: &Remote| a.ty.cmp(&b.ty)) - .into(), - DataTableColumn::new(tr!("AuthId")) - .width("200px") - .render(|item: &Remote| { - html! { - &item.authid - } - }) - .sorter(|a: &Remote, b: &Remote| a.authid.cmp(&b.authid)) - .into(), - DataTableColumn::new(tr!("Nodes")) - .flex(1) - .render(|item: &Remote| { - if item.nodes.is_empty() { - html! {tr!("None")} - } else { - let nodes = item - .nodes - .iter() - .map(|n| n.hostname.as_str()) - .collect::>() - .join(", "); - let mut tip = Column::new(); - tip.add_children(item.nodes.iter().map(|n| { - let text = match n.fingerprint.clone() { - Some(fp) => format!("{} ({fp})", n.hostname), - None => n.hostname.to_string(), - }; - html! {
{text}
} - })); - Tooltip::new(nodes).rich_tip(tip).into() - } - }) - .into(), - /* - DataTableColumn::new(tr!("Auth ID")) - .width("200px") - .render(|item: &Remote| html!{ - item.config.auth_id.clone() - }) - .sorter(|a: &Remote, b: &Remote| { - a.config.auth_id.cmp(&b.config.auth_id) - }) - .into(), - - DataTableColumn::new(tr!("Fingerprint")) - .width("200px") - .render(|item: &Remote| html!{ - item.config.fingerprint.clone().unwrap_or(String::new()) - }) - .into(), - - DataTableColumn::new(tr!("Comment")) - .flex(1) - .render(|item: &Remote| html!{ - item.config.comment.clone().unwrap_or(String::new()) - }) - .into() - */ - ]) -} +mod config; +pub use config::{create_remote, RemoteConfigPanel}; -- 2.39.5 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel