initial commit
This commit is contained in:
292
clients/admin-ui/src/sidebar.rs
Normal file
292
clients/admin-ui/src/sidebar.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use yew::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use gloo::console;
|
||||
use hero_supervisor_openrpc_client::wasm::WasmSupervisorClient;
|
||||
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SupervisorInfo {
|
||||
pub server_url: String,
|
||||
pub admin_secrets_count: usize,
|
||||
pub user_secrets_count: usize,
|
||||
pub register_secrets_count: usize,
|
||||
pub runners_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct SidebarProps {
|
||||
pub server_url: String,
|
||||
pub supervisor_info: Option<SupervisorInfo>,
|
||||
pub admin_secret: String,
|
||||
pub on_admin_secret_change: Callback<String>,
|
||||
pub on_supervisor_info_loaded: Callback<SupervisorInfo>,
|
||||
}
|
||||
|
||||
#[function_component(Sidebar)]
|
||||
pub fn sidebar(props: &SidebarProps) -> Html {
|
||||
let is_unlocked = use_state(|| false);
|
||||
let unlock_secret = use_state(|| String::new());
|
||||
let admin_secrets = use_state(|| Vec::<String>::new());
|
||||
let user_secrets = use_state(|| Vec::<String>::new());
|
||||
let register_secrets = use_state(|| Vec::<String>::new());
|
||||
let is_loading = use_state(|| false);
|
||||
|
||||
|
||||
let on_unlock_secret_change = {
|
||||
let unlock_secret = unlock_secret.clone();
|
||||
Callback::from(move |e: web_sys::Event| {
|
||||
let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
|
||||
unlock_secret.set(input.value());
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
|
||||
let on_unlock_submit = {
|
||||
let unlock_secret = unlock_secret.clone();
|
||||
let is_unlocked = is_unlocked.clone();
|
||||
let is_loading = is_loading.clone();
|
||||
let admin_secrets = admin_secrets.clone();
|
||||
let user_secrets = user_secrets.clone();
|
||||
let register_secrets = register_secrets.clone();
|
||||
let server_url = props.server_url.clone();
|
||||
|
||||
Callback::from(move |_: web_sys::MouseEvent| {
|
||||
let unlock_secret = unlock_secret.clone();
|
||||
let is_unlocked = is_unlocked.clone();
|
||||
let is_loading = is_loading.clone();
|
||||
let admin_secrets = admin_secrets.clone();
|
||||
let user_secrets = user_secrets.clone();
|
||||
let register_secrets = register_secrets.clone();
|
||||
let server_url = server_url.clone();
|
||||
let secret_value = (*unlock_secret).clone();
|
||||
|
||||
if secret_value.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
is_loading.set(true);
|
||||
|
||||
spawn_local(async move {
|
||||
let client = WasmSupervisorClient::new(server_url);
|
||||
|
||||
// Try to load all secrets
|
||||
match client.list_admin_secrets(&secret_value).await {
|
||||
Ok(secrets) => {
|
||||
admin_secrets.set(secrets);
|
||||
|
||||
// Load user secrets
|
||||
if let Ok(user_secs) = client.list_user_secrets(&secret_value).await {
|
||||
user_secrets.set(user_secs);
|
||||
}
|
||||
|
||||
// Load register secrets
|
||||
if let Ok(reg_secs) = client.list_register_secrets(&secret_value).await {
|
||||
register_secrets.set(reg_secs);
|
||||
}
|
||||
|
||||
is_unlocked.set(true);
|
||||
unlock_secret.set(String::new());
|
||||
console::log!("Secrets unlocked successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
console::error!("Failed to unlock secrets:", format!("{:?}", e));
|
||||
}
|
||||
}
|
||||
|
||||
is_loading.set(false);
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
let on_lock_click = {
|
||||
let is_unlocked = is_unlocked.clone();
|
||||
let admin_secrets = admin_secrets.clone();
|
||||
let user_secrets = user_secrets.clone();
|
||||
let register_secrets = register_secrets.clone();
|
||||
|
||||
Callback::from(move |_: web_sys::MouseEvent| {
|
||||
is_unlocked.set(false);
|
||||
admin_secrets.set(Vec::new());
|
||||
user_secrets.set(Vec::new());
|
||||
register_secrets.set(Vec::new());
|
||||
console::log!("Secrets locked");
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>{"Supervisor"}</h2>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<div class="sidebar-sections">
|
||||
// Server Info Section
|
||||
<div class="server-info">
|
||||
<div class="server-header">
|
||||
<h3 class="supervisor-title">{"Hero Supervisor"}</h3>
|
||||
</div>
|
||||
<div class="server-url">
|
||||
<span class="connection-indicator connected"></span>
|
||||
<span class="url-text">{props.server_url.clone()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Secrets Management Section
|
||||
<div class="secrets-section">
|
||||
<div class="secrets-header">
|
||||
<span class="secrets-title">{"Secrets"}</span>
|
||||
if !*is_unlocked {
|
||||
<button
|
||||
class="unlock-btn"
|
||||
onclick={on_unlock_submit}
|
||||
disabled={*is_loading || unlock_secret.is_empty()}
|
||||
>
|
||||
<i class={if *is_loading { "fas fa-spinner fa-spin" } else { "fas fa-unlock" }}></i>
|
||||
</button>
|
||||
} else {
|
||||
<button
|
||||
class="lock-btn"
|
||||
onclick={on_lock_click}
|
||||
>
|
||||
<i class="fas fa-lock"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
if !*is_unlocked {
|
||||
<div class="unlock-input-row">
|
||||
<input
|
||||
type="password"
|
||||
class="unlock-input"
|
||||
placeholder="Enter admin secret to unlock"
|
||||
value={(*unlock_secret).clone()}
|
||||
onchange={on_unlock_secret_change}
|
||||
disabled={*is_loading}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
if *is_unlocked {
|
||||
<div class="secrets-content">
|
||||
<div class="secret-group">
|
||||
<div class="secret-header">
|
||||
<span class="secret-title">{"Admin secrets"}</span>
|
||||
</div>
|
||||
<div class="secret-list">
|
||||
{ for admin_secrets.iter().enumerate().map(|(i, secret)| {
|
||||
html! {
|
||||
<div class="secret-item" key={i}>
|
||||
<div class="secret-value">{secret.clone()}</div>
|
||||
<button class="btn-icon btn-remove">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
<div class="secret-add-row">
|
||||
<input
|
||||
type="text"
|
||||
class="secret-add-input"
|
||||
placeholder="New admin secret"
|
||||
/>
|
||||
<button class="btn-icon btn-add">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="secret-group">
|
||||
<div class="secret-header">
|
||||
<span class="secret-title">{"User secrets"}</span>
|
||||
</div>
|
||||
<div class="secret-list">
|
||||
{ for user_secrets.iter().enumerate().map(|(i, secret)| {
|
||||
html! {
|
||||
<div class="secret-item" key={i}>
|
||||
<div class="secret-value">{secret.clone()}</div>
|
||||
<button class="btn-icon btn-remove">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
<div class="secret-add-row">
|
||||
<input
|
||||
type="text"
|
||||
class="secret-add-input"
|
||||
placeholder="New user secret"
|
||||
/>
|
||||
<button class="btn-icon btn-add">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="secret-group">
|
||||
<div class="secret-header">
|
||||
<span class="secret-title">{"Register secrets"}</span>
|
||||
</div>
|
||||
<div class="secret-list">
|
||||
{ for register_secrets.iter().enumerate().map(|(i, secret)| {
|
||||
html! {
|
||||
<div class="secret-item" key={i}>
|
||||
<div class="secret-value">{secret.clone()}</div>
|
||||
<button class="btn-icon btn-remove">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
<div class="secret-add-row">
|
||||
<input
|
||||
type="text"
|
||||
class="secret-add-input"
|
||||
placeholder="New register secret"
|
||||
/>
|
||||
<button class="btn-icon btn-add">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
if *is_unlocked {
|
||||
<div class="save-section">
|
||||
<button class="save-changes-btn">
|
||||
{"Save Changes"}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Documentation Links at Bottom
|
||||
<div class="sidebar-footer">
|
||||
<div class="docs-section">
|
||||
<h5>{"Documentation"}</h5>
|
||||
<div class="docs-links">
|
||||
<a href="https://github.com/herocode/supervisor" target="_blank" class="doc-link">
|
||||
{"📖 User Guide"}
|
||||
</a>
|
||||
<a href="https://github.com/herocode/supervisor/blob/main/README.md" target="_blank" class="doc-link">
|
||||
{"🚀 Getting Started"}
|
||||
</a>
|
||||
<a href="https://github.com/herocode/supervisor/issues" target="_blank" class="doc-link">
|
||||
{"🐛 Report Issues"}
|
||||
</a>
|
||||
<a href="https://github.com/herocode/supervisor/wiki" target="_blank" class="doc-link">
|
||||
{"📚 API Reference"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user