use crate::messages::UiAction;
use crate::model::AppModel;
use crate::types::{AuthMode, EndpointInfo, SecurityMode};
const BOTTOM_RESERVED: f32 = 56.0;
pub fn draw(model: &AppModel, ctx: &egui::Context, actions: &mut Vec<UiAction>) {
if !model.endpoints_dialog_open {
return;
}
let mut open = true;
egui::Window::new("Connect to OPC UA server")
.open(&mut open)
.resizable(true)
.collapsible(false)
.default_size([820.0, 560.0])
.min_width(560.0)
.min_height(360.0)
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
.show(ctx, |ui| {
draw_contents(ui, model, actions);
});
if !open {
actions.push(UiAction::CloseEndpointPicker);
}
}
fn draw_contents(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
draw_header(ui, model, actions);
ui.separator();
draw_auth(ui, model, actions);
ui.separator();
let total_h = ui.available_height();
let table_h = (total_h - BOTTOM_RESERVED).max(80.0);
ui.allocate_ui_with_layout(
egui::vec2(ui.available_width(), table_h),
egui::Layout::top_down(egui::Align::Min),
|ui| {
ui.set_min_height(table_h);
ui.set_max_height(table_h);
draw_endpoints_area(ui, model, actions);
},
);
ui.separator();
draw_bottom_bar(ui, model, actions);
}
fn draw_header(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
ui.horizontal(|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui
.add_enabled(!model.endpoints_loading, egui::Button::new("Refresh"))
.clicked()
{
actions.push(UiAction::ForceRefreshEndpoints);
}
if model.selected_endpoint.is_some() && ui.button("Clear selection").clicked() {
actions.push(UiAction::ClearSelectedEndpoint);
}
});
});
}
fn draw_auth(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
let (anon_ok, user_ok, cert_ok) = match model.selected_endpoint.as_ref() {
Some(ep) => (
ep.supports_anonymous,
ep.supports_username,
ep.supports_certificate,
),
None => (true, true, true),
};
ui.horizontal(|ui| {
ui.label(egui::RichText::new("Authentication:").strong());
radio_button(ui, model, actions, AuthMode::Anonymous, "Anonymous", anon_ok);
radio_button(
ui,
model,
actions,
AuthMode::UserName,
"Username / Password",
user_ok,
);
radio_button(
ui,
model,
actions,
AuthMode::Certificate,
"X.509 Certificate",
cert_ok,
);
});
match model.auth_mode {
AuthMode::Anonymous => {}
AuthMode::UserName => draw_username_fields(ui, model, actions),
AuthMode::Certificate => draw_certificate_fields(ui, model, actions),
}
}
fn radio_button(
ui: &mut egui::Ui,
model: &AppModel,
actions: &mut Vec<UiAction>,
mode: AuthMode,
label: &str,
enabled: bool,
) {
let selected = model.auth_mode == mode;
let r = ui.add_enabled(enabled, egui::RadioButton::new(selected, label));
if r.clicked() && !selected {
actions.push(UiAction::SetAuthMode(mode));
}
}
fn draw_username_fields(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
ui.horizontal(|ui| {
ui.label("User:");
let mut u = model.auth_username.clone();
if ui
.add(egui::TextEdit::singleline(&mut u).desired_width(200.0))
.changed()
{
actions.push(UiAction::AuthUsernameEdited(u));
}
ui.label("Password:");
let mut p = model.auth_password.clone();
if ui
.add(
egui::TextEdit::singleline(&mut p)
.password(true)
.desired_width(200.0),
)
.changed()
{
actions.push(UiAction::AuthPasswordEdited(p));
}
});
}
fn draw_certificate_fields(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
ui.horizontal(|ui| {
ui.label("Cert path:");
let mut p = model.auth_cert_path.clone();
if ui
.add(egui::TextEdit::singleline(&mut p).desired_width(ui.available_width() - 20.0))
.changed()
{
actions.push(UiAction::AuthCertPathEdited(p));
}
});
ui.horizontal(|ui| {
ui.label("Key path: ");
let mut p = model.auth_key_path.clone();
if ui
.add(egui::TextEdit::singleline(&mut p).desired_width(ui.available_width() - 20.0))
.changed()
{
actions.push(UiAction::AuthKeyPathEdited(p));
}
});
}
fn draw_endpoints_area(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
if model.endpoints_loading {
ui.horizontal(|ui| {
ui.spinner();
ui.label("Querying endpoints…");
});
return;
}
let Some(eps) = model.discovered_endpoints.as_ref() else {
ui.label(egui::RichText::new("Click Refresh to query the server.").italics().weak());
return;
};
if eps.is_empty() {
ui.label("No endpoints returned (server unreachable or discovery failed).");
return;
}
draw_endpoints_table(ui, model, eps, actions);
}
fn draw_endpoints_table(
ui: &mut egui::Ui,
model: &AppModel,
eps: &[EndpointInfo],
actions: &mut Vec<UiAction>,
) {
use egui_extras::{Column, TableBuilder};
let selected_key = model.selected_endpoint.as_ref().map(endpoint_key);
egui::ScrollArea::both().show(ui, |ui| {
TableBuilder::new(ui)
.striped(true)
.resizable(true)
.sense(egui::Sense::click())
.column(Column::auto().at_least(180.0))
.column(Column::auto().at_least(140.0))
.column(Column::auto().at_least(50.0))
.column(Column::auto().at_least(140.0))
.column(Column::remainder().at_least(200.0))
.header(22.0, |mut header| {
header.col(|ui| { ui.strong("Security policy"); });
header.col(|ui| { ui.strong("Mode"); });
header.col(|ui| { ui.strong("Level"); });
header.col(|ui| { ui.strong("Identity tokens"); });
header.col(|ui| { ui.strong("Endpoint URL"); });
})
.body(|mut body| {
for ep in eps {
let is_selected = selected_key.as_ref() == Some(&endpoint_key(ep));
body.row(24.0, |mut row| {
row.set_selected(is_selected);
row.col(|ui| { ui.label(&ep.security_policy); });
row.col(|ui| { ui.label(ep.security_mode.label()); });
row.col(|ui| { ui.label(format!("{}", ep.security_level)); });
row.col(|ui| { ui.label(token_label(ep)); });
row.col(|ui| { ui.label(&ep.endpoint_url); });
if row.response().clicked() && !is_selected {
actions.push(UiAction::SelectEndpoint(ep.clone()));
}
});
}
});
});
}
fn draw_bottom_bar(ui: &mut egui::Ui, model: &AppModel, actions: &mut Vec<UiAction>) {
let ready = model.selected_endpoint.is_some() && auth_ready(model);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let btn = egui::Button::new(egui::RichText::new("Connect").strong())
.min_size(egui::vec2(140.0, 36.0));
if ui
.add_enabled(ready, btn)
.on_hover_text(if ready {
"Connect using the selected endpoint and authentication"
} else {
"Pick an endpoint (and fill credentials if needed)"
})
.clicked()
{
actions.push(UiAction::ConfirmConnect);
}
});
}
fn auth_ready(model: &AppModel) -> bool {
match model.auth_mode {
AuthMode::Anonymous => true,
AuthMode::UserName => !model.auth_username.is_empty(),
AuthMode::Certificate => {
!model.auth_cert_path.is_empty() && !model.auth_key_path.is_empty()
}
}
}
fn endpoint_key(ep: &EndpointInfo) -> (String, String, SecurityMode) {
(
ep.endpoint_url.clone(),
ep.security_policy_uri.clone(),
ep.security_mode,
)
}
fn token_label(ep: &EndpointInfo) -> String {
let mut parts = Vec::new();
if ep.supports_anonymous {
parts.push("Anonymous");
}
if ep.supports_username {
parts.push("UserName");
}
if ep.supports_certificate {
parts.push("Cert");
}
if parts.is_empty() {
"—".to_string()
} else {
parts.join(", ")
}
}