#![cfg(feature = "client")]
use std::sync::Arc;
use terrazzo::autoclone;
use terrazzo::declare_trait_aliias;
use terrazzo::html;
use terrazzo::prelude::*;
use terrazzo::template;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use web_sys::HtmlOptionElement;
use web_sys::HtmlSelectElement;
use super::manager::Manager;
use super::schema::HostPortDefinition;
use super::schema::HostPortDefinitionImpl;
use super::schema::PortForward;
use super::schema::PortForwardStatus;
use super::sync_state::Fields;
use super::sync_state::SyncState;
use crate::api::client_address::ClientAddress;
use crate::assets::icons;
use crate::frontend::menu::menu;
use crate::frontend::remotes::Remote;
use crate::frontend::remotes_ui::show_remote;
use crate::portforward::schema::PortForwardState;
use crate::tiles::signals::TilePtr;
terrazzo_css::import_style!(style, "port_forward.scss");
pub use style::TAG;
#[html]
#[template(tag = div)]
pub fn port_forward(tile: TilePtr) -> XElement {
let manager = Manager::new(tile);
tag(class = style::OUTER, port_forward_impl(manager))
}
#[html]
fn port_forward_impl(manager: Manager) -> XElement {
let tile = manager.tile();
let port_forwards = manager.port_forwards().clone();
div(
class = style::INNER,
key = "port-forward",
div(
class = style::HEADER,
menu(tile.clone()),
show_remote(tile.remote.clone()),
),
show_port_forwards(manager, tile.remote.clone(), port_forwards),
)
}
#[html]
#[template(tag = div)]
fn show_port_forwards(
manager: Manager,
#[signal] remote: Remote,
#[signal] port_forwards: Arc<Vec<PortForward>>,
) -> XElement {
manager.load_port_forwards(remote.clone());
let port_forward_tags = port_forwards
.iter()
.map(|port_forward| show_port_forward(&manager, &remote, port_forward));
let new_sync_state = XSignal::new("new sync-state", Default::default());
tag(
class = style::PORT_FORWARDS,
port_forward_tags..,
div(
show_add_port_forward(new_sync_state.clone()),
click = click_add_port_forward(manager, remote, new_sync_state),
),
)
}
fn click_add_port_forward(
manager: Manager,
remote: Remote,
new_sync_state: XSignal<SyncState>,
) -> impl Fn(web_sys::MouseEvent) {
move |_| {
manager.update(
&remote,
new_sync_state.clone(),
Fields::all() - Fields::DELETE,
|port_forwards| {
let next_id = {
let ids = port_forwards.iter().map(|port_forward| port_forward.id);
ids.max().unwrap_or(0) + 1
};
let port_forwards = port_forwards.iter().cloned();
let next = PortForward {
id: next_id,
..PortForward::default()
};
port_forwards.chain(Some(next)).collect::<Vec<_>>().into()
},
);
}
}
#[html]
#[template(tag = div)]
fn show_add_port_forward(#[signal] new_sync_state: SyncState) -> XElement {
tag(
class = style::ADD,
div(img(src = new_sync_state.add_src())),
"Add port forward",
)
}
#[html]
fn show_port_forward(manager: &Manager, remote: &Remote, port_forward: &PortForward) -> XElement {
let PortForward {
id,
from,
to,
state,
checked: _,
} = port_forward;
let sync_state = XSignal::new("sync-state", SyncState::default());
let params = ShowHostPortDefinition {
manager,
remote,
sync_state: &sync_state,
id: *id,
};
div(
class = style::PORT_FORWARD,
div(
class = style::TITLE,
show_status(sync_state.clone()),
"Listen to traffic from\u{00A0}",
port_forward.from.show(),
"\u{00A0}and forward it to\u{00A0}",
port_forward.to.show(),
show_delete(
manager.clone(),
remote.clone(),
*id,
sync_state.clone(),
sync_state.clone(),
),
),
div(
class = style::PORT_FORWARD_BODY,
div(
class = style::FROM,
show_host_port_definition(params, "From", from, |old, new| {
Some(PortForward {
from: new,
..old.clone()
})
}),
),
div(
class = style::TO,
show_host_port_definition(params, "To", to, |old, new| {
Some(PortForward {
to: new,
..old.clone()
})
}),
),
),
show_active_checkbox(params, port_forward),
show_state(state),
)
}
#[html]
#[autoclone]
fn show_active_checkbox(params: ShowHostPortDefinition, port_forward: &PortForward) -> XElement {
let ShowHostPortDefinition {
manager,
remote,
sync_state,
id,
} = params;
let checked = port_forward.checked;
let toggle_status = move |event: web_sys::Event| {
autoclone!(manager, remote);
autoclone!(sync_state);
let target = event.target().or_throw("targtet for toggle_status");
let target: HtmlInputElement = target.dyn_into().or_throw("input for toggle_status");
let checked = target.checked();
manager.set(
&remote,
sync_state.clone(),
id,
Fields::ACTIVE,
|port_forward| {
Some(PortForward {
checked,
..port_forward.clone()
})
},
)
};
div(
class = style::ACTIVE_CHECKBOX,
label(r#for = format!("active-{id}"), "Active "),
input(
r#type = "checkbox",
id = format!("active-{id}"),
change = toggle_status,
checked = checked.then(|| "checked".to_owned()),
),
)
}
#[html]
fn show_state(state: &PortForwardState) -> XElement {
let state = state.lock();
let count = state.count;
match &state.status {
PortForwardStatus::Pending => div(class = style::PORT_FORWARD_STATUS, "Pending..."),
PortForwardStatus::Up => div(class = style::PORT_FORWARD_STATUS, "Up: {count}"),
PortForwardStatus::Offline => {
if count == 0 {
div(class = style::PORT_FORWARD_STATUS, "Offline")
} else {
div(
class = style::PORT_FORWARD_STATUS,
"Pending shutdown: {count}",
)
}
}
PortForwardStatus::Failed(error) => div(
class = style::PORT_FORWARD_STATUS,
span(style::color = "red", style::font_weight = "bold", "Error: "),
"{error}",
),
}
}
#[html]
#[template(tag = img)]
fn show_status(#[signal] sync_state: SyncState) -> XElement {
tag(class = style::STATUS, src = sync_state.status_src())
}
#[autoclone]
#[html]
#[template(tag = img)]
fn show_delete(
manager: Manager,
remote: Remote,
id: i32,
sync_state_signal: XSignal<SyncState>,
#[signal] sync_state: SyncState,
) -> XElement {
tag(
class = style::DELETE,
style::visibility = sync_state.is_deleting().then_some("hidden"),
src = icons::trash(),
click = move |_| {
autoclone!(remote);
manager.update(
&remote,
sync_state_signal.clone(),
Fields::DELETE,
|port_forwards| {
port_forwards
.iter()
.filter(|&port_forward| port_forward.id != id)
.cloned()
.collect::<Vec<_>>()
.into()
},
);
},
)
}
#[derive(Clone, Copy)]
struct ShowHostPortDefinition<'t> {
manager: &'t Manager,
remote: &'t Remote,
sync_state: &'t XSignal<SyncState>,
id: i32,
}
declare_trait_aliias! {
EditHostPortDefinition,
FnOnce(&PortForward, HostPortDefinition) -> Option<PortForward> + Clone + 'static
}
#[autoclone]
#[html]
fn show_host_port_definition(
params: ShowHostPortDefinition,
endpoint: &'static str,
host_port_definition: &HostPortDefinition,
set: impl EditHostPortDefinition,
) -> XElement {
let ShowHostPortDefinition {
manager,
remote,
sync_state,
id,
} = params;
let HostPortDefinitionImpl {
forwarded_remote,
host,
port,
} = &**host_port_definition;
let port = *port;
let set_remote = move |forwarded_remote| {
autoclone!(manager, remote);
autoclone!(host, set, sync_state);
manager.set(
&remote,
sync_state.clone(),
id,
Fields::REMOTE,
move |port_forward| {
autoclone!(host, set);
let new = HostPortDefinition::new(forwarded_remote, host.clone(), port);
set(port_forward, new)
},
);
};
let set_host = move |event: web_sys::Event| {
autoclone!(manager, remote);
autoclone!(forwarded_remote, set, sync_state);
let target = event.target().or_throw("targtet for set_host");
let target: HtmlInputElement = target.dyn_into().or_throw("input for set_host");
manager.set(
&remote,
sync_state.clone(),
id,
Fields::HOST,
|port_forward| {
autoclone!(forwarded_remote, set);
let new = HostPortDefinition::new(
forwarded_remote,
target.value().trim().to_owned(),
port,
);
set(port_forward, new)
},
)
};
let set_port = move |event: web_sys::Event| {
autoclone!(manager, remote);
autoclone!(forwarded_remote, host, set, sync_state);
let target = event.target().or_throw("targtet for set_port");
let target: HtmlInputElement = target.dyn_into().or_throw("input for set_port");
let port = target.value();
let Ok(port) = port.trim().parse() else {
diagnostics::warn!("Value doesn't parse as u16: '{port}'");
return;
};
manager.set(
&remote,
sync_state.clone(),
id,
Fields::PORT,
|port_forward| {
autoclone!(forwarded_remote, host, set);
let new = HostPortDefinition::new(forwarded_remote, host, port);
set(port_forward, new)
},
)
};
div(
class = style::HOST_PORT_DEFINITION,
div(class = style::ENDPOINT, "{endpoint}"),
div(
class = style::REMOTE,
label(r#for = format!("remote-{id}"), "Remote: "),
show_remote_select(
format!("host-{id}"),
manager.remotes(),
forwarded_remote.clone(),
set_remote,
),
),
div(
class = style::HOST,
label(r#for = format!("host-{id}"), "Host: "),
input(
r#type = "text",
id = format!("host-{id}"),
value = host.to_owned(),
change = set_host,
keydown = move |_event| {
autoclone!(sync_state);
SyncState::incr_pending(sync_state.clone(), Fields::HOST)
},
blur = move |_event| {
autoclone!(sync_state);
SyncState::decr_pending(sync_state.clone(), Fields::HOST)
},
autocomplete = "off",
),
),
div(
class = style::PORT,
label(r#for = format!("port-{id}"), "Port: "),
input(
r#type = "number",
min = "1",
max = "65535",
id = format!("port-{id}"),
value = host_port_definition.port.to_string(),
change = set_port,
keydown = move |_event| {
autoclone!(sync_state);
SyncState::incr_pending(sync_state.clone(), Fields::PORT)
},
blur = move |_event| {
autoclone!(sync_state);
SyncState::decr_pending(sync_state.clone(), Fields::PORT)
},
autocomplete = "off",
),
),
)
}
#[html]
#[template(tag = select)]
fn show_remote_select(
tag_id: String,
#[signal] remotes: Vec<ClientAddress>,
selected: ClientAddress,
set: impl Fn(ClientAddress) + Clone + 'static,
) -> XElement {
let mut options = vec![];
static LOCAL: &str = "Local";
let mut selected_index = 0;
options.push(option(value = "", "{LOCAL}"));
for (i, remote) in remotes.iter().enumerate() {
if *remote == selected {
selected_index = options.len(); }
options.push(option(value = i.to_string(), "{remote}"))
}
if !selected.is_empty() && selected_index == 0 {
selected_index = options.len();
options.push(option(
value = format!("{selected} (offline)"),
"{selected} (offline)",
after_render = |option| {
let option: &HtmlOptionElement = option.dyn_ref().or_throw("option");
option.set_disabled(true);
},
));
}
tag(
id = tag_id,
change = move |ev: web_sys::Event| {
let select = ev.target().or_throw("remote target");
let select: web_sys::HtmlSelectElement = select.dyn_into().or_throw("remote select");
let value = select.value();
if value.is_empty() {
set(ClientAddress::default());
} else {
let value: usize = value.parse().or_throw("remote index");
set(remotes[value].clone());
}
},
after_render = move |select| {
let select: &HtmlSelectElement = select.dyn_ref().or_throw("select");
select.set_selected_index(selected_index as i32);
},
options..,
)
}