remove unused dep and move job out
This commit is contained in:
153
Cargo.lock
generated
153
Cargo.lock
generated
@@ -331,15 +331,6 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "deranged"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
|
||||||
dependencies = [
|
|
||||||
"powerfmt",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -437,7 +428,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-executor",
|
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
@@ -460,17 +450,6 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-executor"
|
|
||||||
version = "0.3.31"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -596,21 +575,50 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hero-job"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"log",
|
||||||
|
"redis",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hero-job"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://git.ourworld.tf/herocode/job#1f7cd4ded8db57fb5ec3f7d42782fe45a70af164"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"log",
|
||||||
|
"redis",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hero-supervisor"
|
name = "hero-supervisor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger 0.10.2",
|
"env_logger 0.10.2",
|
||||||
"escargot",
|
"escargot",
|
||||||
|
"hero-job 0.1.0 (git+https://git.ourworld.tf/herocode/job)",
|
||||||
"hero-supervisor-openrpc-client",
|
"hero-supervisor-openrpc-client",
|
||||||
"jsonrpsee",
|
"jsonrpsee",
|
||||||
"log",
|
"log",
|
||||||
|
"rand",
|
||||||
"redis",
|
"redis",
|
||||||
"sal-service-manager",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -630,6 +638,7 @@ dependencies = [
|
|||||||
"console_log",
|
"console_log",
|
||||||
"env_logger 0.11.8",
|
"env_logger 0.11.8",
|
||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
|
"hero-job 0.1.0",
|
||||||
"hero-supervisor",
|
"hero-supervisor",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"jsonrpsee",
|
"jsonrpsee",
|
||||||
@@ -1157,12 +1166,6 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-conv"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -1260,19 +1263,6 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "plist"
|
|
||||||
version = "1.7.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1"
|
|
||||||
dependencies = [
|
|
||||||
"base64",
|
|
||||||
"indexmap",
|
|
||||||
"quick-xml",
|
|
||||||
"serde",
|
|
||||||
"time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -1297,12 +1287,6 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "powerfmt"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -1330,15 +1314,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quick-xml"
|
|
||||||
version = "0.38.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d200a41a7797e6461bd04e4e95c3347053a731c32c87f066f2f0dda22dbdbba8"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
@@ -1561,23 +1536,6 @@ version = "1.0.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sal-service-manager"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"chrono",
|
|
||||||
"futures",
|
|
||||||
"log",
|
|
||||||
"once_cell",
|
|
||||||
"plist",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"zinit-client",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -1815,37 +1773,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time"
|
|
||||||
version = "0.3.41"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
|
||||||
dependencies = [
|
|
||||||
"deranged",
|
|
||||||
"itoa",
|
|
||||||
"num-conv",
|
|
||||||
"powerfmt",
|
|
||||||
"serde",
|
|
||||||
"time-core",
|
|
||||||
"time-macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time-core"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time-macros"
|
|
||||||
version = "0.2.22"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
|
||||||
dependencies = [
|
|
||||||
"num-conv",
|
|
||||||
"time-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2667,21 +2594,3 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zinit-client"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4121c3ba22f1b3ccc4546de32072c9530c7e2735b734641ada5280ac422ac9cd"
|
|
||||||
dependencies = [
|
|
||||||
"async-stream",
|
|
||||||
"async-trait",
|
|
||||||
"chrono",
|
|
||||||
"futures",
|
|
||||||
"rand",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
@@ -4,6 +4,8 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# Shared job crate
|
||||||
|
hero-job = { git = "https://git.ourworld.tf/herocode/job" }
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
|
||||||
@@ -23,7 +25,6 @@ chrono = "0.4"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
sal-service-manager = { path = "../sal/service_manager" }
|
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
clap = { version = "4.0", features = ["derive"] }
|
clap = { version = "4.0", features = ["derive"] }
|
||||||
@@ -37,6 +38,12 @@ anyhow = "1.0"
|
|||||||
tower-http = { version = "0.5", features = ["cors"] }
|
tower-http = { version = "0.5", features = ["cors"] }
|
||||||
tower = "0.4"
|
tower = "0.4"
|
||||||
|
|
||||||
|
# Base64 encoding for Mycelium payloads
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
|
# Random number generation for message IDs
|
||||||
|
rand = "0.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
hero-supervisor-openrpc-client = { path = "clients/openrpc" }
|
hero-supervisor-openrpc-client = { path = "clients/openrpc" }
|
||||||
|
14
clients/admin-ui/Cargo.lock
generated
14
clients/admin-ui/Cargo.lock
generated
@@ -1025,6 +1025,19 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hero-job"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"log",
|
||||||
|
"redis",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hero-supervisor"
|
name = "hero-supervisor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1034,6 +1047,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger 0.10.2",
|
"env_logger 0.10.2",
|
||||||
|
"hero-job",
|
||||||
"jsonrpsee",
|
"jsonrpsee",
|
||||||
"log",
|
"log",
|
||||||
"redis",
|
"redis",
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Hero Supervisor</title>
|
<title>Hero Supervisor</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
<link data-trunk rel="css" href="styles.css">
|
<link data-trunk rel="css" href="styles.css">
|
||||||
</head>
|
</head>
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use gloo::console;
|
use gloo::console;
|
||||||
use gloo::timers::callback::Interval;
|
use gloo::timers::callback::Interval;
|
||||||
|
use gloo::storage::{LocalStorage, Storage};
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob};
|
use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob};
|
||||||
use crate::sidebar::{Sidebar, SupervisorInfo, SessionSecretType};
|
use crate::sidebar::{Sidebar, SupervisorInfo, SessionSecretType, SessionData, SESSION_STORAGE_KEY};
|
||||||
use crate::runners::{Runners, RegisterForm};
|
use crate::runners::{Runners, RegisterForm};
|
||||||
use crate::jobs::Jobs;
|
use crate::jobs::Jobs;
|
||||||
|
use crate::toast::{Toast, ToastContainer};
|
||||||
|
|
||||||
/// Generate a unique job ID client-side using UUID v4
|
/// Generate a unique job ID client-side using UUID v4
|
||||||
fn generate_job_id() -> String {
|
fn generate_job_id() -> String {
|
||||||
@@ -17,7 +19,6 @@ pub struct JobForm {
|
|||||||
pub payload: String,
|
pub payload: String,
|
||||||
pub runner: String,
|
pub runner: String,
|
||||||
pub executor: String,
|
pub executor: String,
|
||||||
pub secret: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -45,7 +46,10 @@ pub struct AppState {
|
|||||||
pub job_form: JobForm,
|
pub job_form: JobForm,
|
||||||
pub supervisor_info: Option<SupervisorInfo>,
|
pub supervisor_info: Option<SupervisorInfo>,
|
||||||
pub admin_secret: String,
|
pub admin_secret: String,
|
||||||
|
pub session_secret: String,
|
||||||
|
pub session_secret_type: SessionSecretType,
|
||||||
pub ping_states: std::collections::HashMap<String, PingState>, // runner -> ping_state
|
pub ping_states: std::collections::HashMap<String, PingState>, // runner -> ping_state
|
||||||
|
pub toasts: Vec<Toast>, // Toast notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -54,25 +58,36 @@ pub struct AppState {
|
|||||||
|
|
||||||
#[function_component(App)]
|
#[function_component(App)]
|
||||||
pub fn app() -> Html {
|
pub fn app() -> Html {
|
||||||
let state = use_state(|| AppState {
|
let state = use_state(|| {
|
||||||
server_url: "http://localhost:3030".to_string(),
|
// Try to load session from localStorage
|
||||||
runners: vec![],
|
let (session_secret, session_secret_type) = if let Ok(session_data) = LocalStorage::get::<SessionData>(SESSION_STORAGE_KEY) {
|
||||||
jobs: vec![],
|
(session_data.secret, session_data.secret_type)
|
||||||
ongoing_jobs: vec![],
|
} else {
|
||||||
loading: false,
|
(String::new(), SessionSecretType::None)
|
||||||
register_form: RegisterForm {
|
};
|
||||||
name: String::new(),
|
|
||||||
secret: String::new(),
|
AppState {
|
||||||
},
|
server_url: "http://localhost:3030".to_string(),
|
||||||
job_form: JobForm {
|
runners: vec![],
|
||||||
payload: String::new(),
|
jobs: vec![],
|
||||||
runner: String::new(),
|
ongoing_jobs: vec![],
|
||||||
executor: String::new(),
|
loading: false,
|
||||||
secret: String::new(),
|
register_form: RegisterForm {
|
||||||
},
|
name: String::new(),
|
||||||
supervisor_info: None,
|
secret: String::new(),
|
||||||
admin_secret: String::new(),
|
},
|
||||||
ping_states: std::collections::HashMap::new(),
|
job_form: JobForm {
|
||||||
|
payload: String::new(),
|
||||||
|
runner: String::new(),
|
||||||
|
executor: String::new(),
|
||||||
|
},
|
||||||
|
supervisor_info: None,
|
||||||
|
admin_secret: String::new(),
|
||||||
|
session_secret,
|
||||||
|
session_secret_type,
|
||||||
|
ping_states: std::collections::HashMap::new(),
|
||||||
|
toasts: vec![],
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up polling for ongoing jobs every 2 seconds
|
// Set up polling for ongoing jobs every 2 seconds
|
||||||
@@ -131,6 +146,90 @@ pub fn app() -> Html {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check server connection status periodically
|
||||||
|
{
|
||||||
|
let state = state.clone();
|
||||||
|
use_effect_with((), move |_| {
|
||||||
|
let state = state.clone();
|
||||||
|
let client_url = state.server_url.clone();
|
||||||
|
|
||||||
|
let check_connection = {
|
||||||
|
let state = state.clone();
|
||||||
|
let client_url = client_url.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
let state = state.clone();
|
||||||
|
let client_url = client_url.clone();
|
||||||
|
let client = WasmSupervisorClient::new(client_url.clone());
|
||||||
|
spawn_local(async move {
|
||||||
|
let mut current_state = (*state).clone();
|
||||||
|
|
||||||
|
// Try to ping the server to check connection
|
||||||
|
match client.list_runners().await {
|
||||||
|
Ok(_) => {
|
||||||
|
// Server is reachable, now try to load secrets if we have a session secret
|
||||||
|
let mut admin_secrets = vec![];
|
||||||
|
let mut user_secrets = vec![];
|
||||||
|
let mut register_secrets = vec![];
|
||||||
|
|
||||||
|
// Try to load secrets based on current session secret
|
||||||
|
if !current_state.session_secret.is_empty() {
|
||||||
|
match current_state.session_secret_type {
|
||||||
|
SessionSecretType::Admin => {
|
||||||
|
if let Ok(secrets) = client.list_admin_secrets(¤t_state.session_secret).await {
|
||||||
|
admin_secrets = secrets;
|
||||||
|
}
|
||||||
|
if let Ok(secrets) = client.list_user_secrets(¤t_state.session_secret).await {
|
||||||
|
user_secrets = secrets;
|
||||||
|
}
|
||||||
|
if let Ok(secrets) = client.list_register_secrets(¤t_state.session_secret).await {
|
||||||
|
register_secrets = secrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionSecretType::User => {
|
||||||
|
if let Ok(secrets) = client.list_user_secrets(¤t_state.session_secret).await {
|
||||||
|
user_secrets = secrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionSecretType::Register => {
|
||||||
|
if let Ok(secrets) = client.list_register_secrets(¤t_state.session_secret).await {
|
||||||
|
register_secrets = secrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionSecretType::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_state.supervisor_info = Some(SupervisorInfo {
|
||||||
|
server_url: client_url.clone(),
|
||||||
|
runners_count: 0,
|
||||||
|
admin_secrets,
|
||||||
|
user_secrets,
|
||||||
|
register_secrets,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Server is not reachable
|
||||||
|
current_state.supervisor_info = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.set(current_state);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check connection immediately
|
||||||
|
check_connection.emit(());
|
||||||
|
|
||||||
|
// Set up interval to check connection every 5 seconds
|
||||||
|
let interval = Interval::new(5000, move || {
|
||||||
|
check_connection.emit(());
|
||||||
|
});
|
||||||
|
|
||||||
|
move || drop(interval)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Load initial data when component mounts
|
// Load initial data when component mounts
|
||||||
let load_initial_data = {
|
let load_initial_data = {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
@@ -262,9 +361,10 @@ pub fn app() -> Html {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
console::error!("Failed to load runners:", format!("{:?}", e));
|
console::error!("Failed to load runners:", format!("{:?}", e));
|
||||||
let mut updated_state = (*state).clone();
|
let mut error_state = (*state).clone();
|
||||||
updated_state.loading = false;
|
error_state.loading = false;
|
||||||
state.set(updated_state);
|
error_state.toasts.push(Toast::error(format!("Failed to load runners: {:?}", e)));
|
||||||
|
state.set(error_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -294,7 +394,10 @@ pub fn app() -> Html {
|
|||||||
job_form: state.job_form.clone(),
|
job_form: state.job_form.clone(),
|
||||||
supervisor_info: state.supervisor_info.clone(),
|
supervisor_info: state.supervisor_info.clone(),
|
||||||
admin_secret: state.admin_secret.clone(),
|
admin_secret: state.admin_secret.clone(),
|
||||||
|
session_secret: state.session_secret.clone(),
|
||||||
|
session_secret_type: state.session_secret_type.clone(),
|
||||||
ping_states: state.ping_states.clone(),
|
ping_states: state.ping_states.clone(),
|
||||||
|
toasts: state.toasts.clone(),
|
||||||
};
|
};
|
||||||
state.set(new_state);
|
state.set(new_state);
|
||||||
})
|
})
|
||||||
@@ -354,7 +457,6 @@ pub fn app() -> Html {
|
|||||||
"payload" => new_form.payload = value,
|
"payload" => new_form.payload = value,
|
||||||
"runner" => new_form.runner = value,
|
"runner" => new_form.runner = value,
|
||||||
"executor" => new_form.executor = value,
|
"executor" => new_form.executor = value,
|
||||||
"secret" => new_form.secret = value,
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
let mut new_state = (*state).clone();
|
let mut new_state = (*state).clone();
|
||||||
@@ -366,7 +468,7 @@ pub fn app() -> Html {
|
|||||||
// Run job callback - now uses create_job for immediate display and polling
|
// Run job callback - now uses create_job for immediate display and polling
|
||||||
let on_run_job = {
|
let on_run_job = {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
Callback::from(move |_| {
|
Callback::from(move |_: ()| {
|
||||||
let current_state = (*state).clone();
|
let current_state = (*state).clone();
|
||||||
let client = WasmSupervisorClient::new(current_state.server_url.clone());
|
let client = WasmSupervisorClient::new(current_state.server_url.clone());
|
||||||
let job_form = current_state.job_form.clone();
|
let job_form = current_state.job_form.clone();
|
||||||
@@ -396,7 +498,7 @@ pub fn app() -> Html {
|
|||||||
console::log!("Job added to list immediately with ID:", &job_id);
|
console::log!("Job added to list immediately with ID:", &job_id);
|
||||||
|
|
||||||
// Create the job using fire-and-forget create_job method
|
// Create the job using fire-and-forget create_job method
|
||||||
match client.create_job(job_form.secret.clone(), job).await {
|
match client.create_job(current_state.session_secret.clone(), job).await {
|
||||||
Ok(returned_job_id) => {
|
Ok(returned_job_id) => {
|
||||||
console::log!("Job created successfully with ID:", &returned_job_id);
|
console::log!("Job created successfully with ID:", &returned_job_id);
|
||||||
}
|
}
|
||||||
@@ -415,10 +517,71 @@ pub fn app() -> Html {
|
|||||||
// Supervisor info loaded callback
|
// Supervisor info loaded callback
|
||||||
let on_supervisor_info_loaded = {
|
let on_supervisor_info_loaded = {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
Callback::from(move |supervisor_info: SupervisorInfo| {
|
Callback::from(move |info: SupervisorInfo| {
|
||||||
let mut new_state = (*state).clone();
|
let mut current_state = (*state).clone();
|
||||||
new_state.supervisor_info = Some(supervisor_info);
|
current_state.supervisor_info = Some(info);
|
||||||
state.set(new_state);
|
state.set(current_state);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_add_secret = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |(secret_type, _secret): (SessionSecretType, String)| {
|
||||||
|
let mut current_state = (*state).clone();
|
||||||
|
if let Some(ref mut info) = current_state.supervisor_info {
|
||||||
|
// Prompt for a new secret
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
if let Ok(Some(new_secret)) = window.prompt_with_message("Enter new secret:") {
|
||||||
|
if !new_secret.is_empty() {
|
||||||
|
match secret_type {
|
||||||
|
SessionSecretType::Admin => {
|
||||||
|
if !info.admin_secrets.contains(&new_secret) {
|
||||||
|
info.admin_secrets.push(new_secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionSecretType::User => {
|
||||||
|
if !info.user_secrets.contains(&new_secret) {
|
||||||
|
info.user_secrets.push(new_secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionSecretType::Register => {
|
||||||
|
if !info.register_secrets.contains(&new_secret) {
|
||||||
|
info.register_secrets.push(new_secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionSecretType::None => {
|
||||||
|
// Do nothing for None type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.set(current_state);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_remove_secret = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |(secret_type, secret): (SessionSecretType, String)| {
|
||||||
|
let mut current_state = (*state).clone();
|
||||||
|
if let Some(ref mut info) = current_state.supervisor_info {
|
||||||
|
match secret_type {
|
||||||
|
SessionSecretType::Admin => {
|
||||||
|
info.admin_secrets.retain(|s| s != &secret);
|
||||||
|
}
|
||||||
|
SessionSecretType::User => {
|
||||||
|
info.user_secrets.retain(|s| s != &secret);
|
||||||
|
}
|
||||||
|
SessionSecretType::Register => {
|
||||||
|
info.register_secrets.retain(|s| s != &secret);
|
||||||
|
}
|
||||||
|
SessionSecretType::None => {
|
||||||
|
// Do nothing for None type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.set(current_state);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -439,10 +602,14 @@ pub fn app() -> Html {
|
|||||||
// Remove runner from the list
|
// Remove runner from the list
|
||||||
let mut updated_state = (*state_clone).clone();
|
let mut updated_state = (*state_clone).clone();
|
||||||
updated_state.runners.retain(|(name, _)| name != &runner_id);
|
updated_state.runners.retain(|(name, _)| name != &runner_id);
|
||||||
|
updated_state.toasts.push(Toast::success("Runner removed successfully".to_string()));
|
||||||
state_clone.set(updated_state);
|
state_clone.set(updated_state);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
console::error!("Failed to remove runner:", format!("{:?}", e));
|
console::error!("Failed to remove runner:", format!("{:?}", e));
|
||||||
|
let mut error_state = (*state_clone).clone();
|
||||||
|
error_state.toasts.push(Toast::error(format!("Failed to remove runner: {:?}", e)));
|
||||||
|
state_clone.set(error_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -470,6 +637,9 @@ pub fn app() -> Html {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
console::error!("Failed to stop job:", format!("{:?}", e));
|
console::error!("Failed to stop job:", format!("{:?}", e));
|
||||||
|
let mut error_state = (*state_clone).clone();
|
||||||
|
error_state.toasts.push(Toast::error(format!("Failed to stop job: {:?}", e)));
|
||||||
|
state_clone.set(error_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -498,16 +668,112 @@ pub fn app() -> Html {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
console::error!("Failed to delete job:", format!("{:?}", e));
|
console::error!("Failed to delete job:", format!("{:?}", e));
|
||||||
|
let mut error_state = (*state_clone).clone();
|
||||||
|
error_state.toasts.push(Toast::error(format!("Failed to delete job: {:?}", e)));
|
||||||
|
state_clone.set(error_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ping runner callback - uses run_job for immediate result with proper state management
|
// Toast dismiss callback
|
||||||
let on_ping_runner = {
|
let on_toast_dismiss = {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
Callback::from(move |(runner_id, secret): (String, String)| {
|
Callback::from(move |toast_id: String| {
|
||||||
|
let mut current_state = (*state).clone();
|
||||||
|
current_state.toasts.retain(|t| t.id != toast_id);
|
||||||
|
state.set(current_state);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add toast callback
|
||||||
|
let add_toast = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |toast: Toast| {
|
||||||
|
let mut current_state = (*state).clone();
|
||||||
|
current_state.toasts.push(toast);
|
||||||
|
state.set(current_state);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Session secret change callback
|
||||||
|
let on_session_secret_change = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |(secret, secret_type): (String, SessionSecretType)| {
|
||||||
|
let mut current_state = (*state).clone();
|
||||||
|
current_state.session_secret = secret.clone();
|
||||||
|
current_state.session_secret_type = secret_type.clone();
|
||||||
|
|
||||||
|
// If we have a session secret, trigger API call to load secrets
|
||||||
|
if !secret.is_empty() {
|
||||||
|
let client = WasmSupervisorClient::new(current_state.server_url.clone());
|
||||||
|
let state_clone = state.clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
let mut updated_state = (*state_clone).clone();
|
||||||
|
|
||||||
|
// Try to load secrets from API based on secret type
|
||||||
|
match client.list_runners().await {
|
||||||
|
Ok(_) => {
|
||||||
|
let mut admin_secrets = vec![];
|
||||||
|
let mut user_secrets = vec![];
|
||||||
|
let mut register_secrets = vec![];
|
||||||
|
|
||||||
|
// Load secrets based on secret type
|
||||||
|
match secret_type {
|
||||||
|
SessionSecretType::Admin => {
|
||||||
|
if let Ok(secrets) = client.list_admin_secrets(&secret).await {
|
||||||
|
admin_secrets = secrets;
|
||||||
|
}
|
||||||
|
if let Ok(secrets) = client.list_user_secrets(&secret).await {
|
||||||
|
user_secrets = secrets;
|
||||||
|
}
|
||||||
|
if let Ok(secrets) = client.list_register_secrets(&secret).await {
|
||||||
|
register_secrets = secrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionSecretType::User => {
|
||||||
|
if let Ok(secrets) = client.list_user_secrets(&secret).await {
|
||||||
|
user_secrets = secrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionSecretType::Register => {
|
||||||
|
if let Ok(secrets) = client.list_register_secrets(&secret).await {
|
||||||
|
register_secrets = secrets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SessionSecretType::None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated_state.supervisor_info = Some(SupervisorInfo {
|
||||||
|
server_url: updated_state.server_url.clone(),
|
||||||
|
runners_count: 0,
|
||||||
|
admin_secrets,
|
||||||
|
user_secrets,
|
||||||
|
register_secrets,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
updated_state.supervisor_info = None;
|
||||||
|
// Add error toast
|
||||||
|
let error_msg = format!("Failed to connect to supervisor: {:?}", e);
|
||||||
|
updated_state.toasts.push(Toast::error(error_msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state_clone.set(updated_state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
state.set(current_state);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run job callback
|
||||||
|
let on_run_job = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |(runner_id, payload): (String, String)| {
|
||||||
let current_state = (*state).clone();
|
let current_state = (*state).clone();
|
||||||
let client = WasmSupervisorClient::new(current_state.server_url.clone());
|
let client = WasmSupervisorClient::new(current_state.server_url.clone());
|
||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
@@ -520,26 +786,22 @@ pub fn app() -> Html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
console::log!("Pinging runner:", &runner_id);
|
console::log!("Running job on runner:", &runner_id, "with payload:", &payload);
|
||||||
|
|
||||||
// Generate unique job ID client-side
|
// Use session secret to create job with payload
|
||||||
let job_id = generate_job_id();
|
let job = WasmJob::new(
|
||||||
|
generate_job_id(),
|
||||||
// Create ping job with client-generated ID
|
payload.clone(),
|
||||||
let ping_job = WasmJob::new(
|
"default".to_string(),
|
||||||
job_id.clone(),
|
runner_id.clone()
|
||||||
"ping".to_string(),
|
|
||||||
"ping".to_string(),
|
|
||||||
runner_id.clone(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use run_job for immediate result instead of create_job
|
match client.run_job(current_state.session_secret, job).await {
|
||||||
match client.run_job(secret, ping_job).await {
|
Ok(job_id) => {
|
||||||
Ok(result) => {
|
console::log!("Job created successfully:", &job_id);
|
||||||
console::log!("Ping successful, result:", &result);
|
// Set ping state to success with job ID
|
||||||
// Set ping state to success with result
|
|
||||||
let mut success_state = (*state_clone).clone();
|
let mut success_state = (*state_clone).clone();
|
||||||
success_state.ping_states.insert(runner_id.clone(), PingState::Success(result));
|
success_state.ping_states.insert(runner_id.clone(), PingState::Success(format!("Job created: {}", job_id)));
|
||||||
state_clone.set(success_state);
|
state_clone.set(success_state);
|
||||||
|
|
||||||
// Reset to idle after 3 seconds
|
// Reset to idle after 3 seconds
|
||||||
@@ -553,7 +815,7 @@ pub fn app() -> Html {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
console::error!("Failed to ping runner:", format!("{:?}", e));
|
console::error!("Failed to create job:", format!("{:?}", e));
|
||||||
// Set ping state to error
|
// Set ping state to error
|
||||||
let mut error_state = (*state_clone).clone();
|
let mut error_state = (*state_clone).clone();
|
||||||
let error_msg = format!("Error: {:?}", e);
|
let error_msg = format!("Error: {:?}", e);
|
||||||
@@ -585,45 +847,60 @@ pub fn app() -> Html {
|
|||||||
});
|
});
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="app-container">
|
<div class="bg-dark min-vh-100">
|
||||||
<Sidebar
|
<div class="container-fluid h-100">
|
||||||
server_url={state.server_url.clone()}
|
<div class="row g-0 h-100">
|
||||||
supervisor_info={state.supervisor_info.clone()}
|
<Sidebar
|
||||||
session_secret={state.admin_secret.clone()}
|
server_url={state.server_url.clone()}
|
||||||
session_secret_type={SessionSecretType::Admin}
|
supervisor_info={state.supervisor_info.clone()}
|
||||||
on_session_secret_change={on_admin_secret_change}
|
session_secret={state.session_secret.clone()}
|
||||||
on_supervisor_info_loaded={on_supervisor_info_loaded}
|
session_secret_type={state.session_secret_type.clone()}
|
||||||
/>
|
on_session_secret_change={on_session_secret_change}
|
||||||
|
on_supervisor_info_loaded={on_supervisor_info_loaded}
|
||||||
<div class="main-content">
|
on_add_secret={on_add_secret}
|
||||||
<Runners
|
on_remove_secret={on_remove_secret}
|
||||||
server_url={state.server_url.clone()}
|
/>
|
||||||
runners={state.runners.clone()}
|
|
||||||
register_form={state.register_form.clone()}
|
<main class="col-md-9 col-lg-10 overflow-auto h-100">
|
||||||
ping_states={state.ping_states.clone()}
|
<div class="p-4 main-content">
|
||||||
on_register_form_change={on_register_form_change}
|
<Runners
|
||||||
on_register_runner={on_register_runner}
|
server_url={state.server_url.clone()}
|
||||||
on_load_runners={on_load_runners.clone()}
|
runners={state.runners.clone()}
|
||||||
on_remove_runner={on_remove_runner}
|
register_form={state.register_form.clone()}
|
||||||
on_ping_runner={on_ping_runner}
|
ping_states={state.ping_states.clone()}
|
||||||
/>
|
on_register_form_change={on_register_form_change}
|
||||||
|
on_register_runner={on_register_runner}
|
||||||
<Jobs
|
on_load_runners={on_load_runners.clone()}
|
||||||
jobs={state.jobs.clone()}
|
on_remove_runner={on_remove_runner}
|
||||||
server_url={state.server_url.clone()}
|
session_secret={state.session_secret.clone()}
|
||||||
job_form={state.job_form.clone()}
|
on_run_job={on_run_job.clone()}
|
||||||
runners={state.runners.clone()}
|
/>
|
||||||
on_job_form_change={on_job_form_change}
|
|
||||||
on_run_job={on_run_job}
|
<Jobs
|
||||||
on_stop_job={on_stop_job}
|
jobs={state.jobs.clone()}
|
||||||
on_delete_job={on_delete_job}
|
server_url={state.server_url.clone()}
|
||||||
/>
|
job_form={state.job_form.clone()}
|
||||||
|
runners={state.runners.clone()}
|
||||||
// Floating refresh button
|
on_job_form_change={on_job_form_change}
|
||||||
<button class="refresh-btn" onclick={on_load_runners.reform(|_| ())}>
|
on_run_job={on_run_job}
|
||||||
{"↻"}
|
on_stop_job={on_stop_job}
|
||||||
</button>
|
on_delete_job={on_delete_job}
|
||||||
|
/>
|
||||||
|
|
||||||
|
// Floating refresh button
|
||||||
|
<button class="btn btn-primary position-fixed" style="bottom: 20px; right: 20px; z-index: 1000;" onclick={on_load_runners.reform(|_| ())}>
|
||||||
|
{"↻"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
// Toast notifications
|
||||||
|
<ToastContainer
|
||||||
|
toasts={state.toasts.clone()}
|
||||||
|
on_dismiss={on_toast_dismiss}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ pub struct JobsProps {
|
|||||||
pub job_form: JobForm,
|
pub job_form: JobForm,
|
||||||
pub runners: Vec<(String, String)>, // (name, status) - list of registered runners
|
pub runners: Vec<(String, String)>, // (name, status) - list of registered runners
|
||||||
pub on_job_form_change: Callback<(String, String)>,
|
pub on_job_form_change: Callback<(String, String)>,
|
||||||
pub on_run_job: Callback<()>,
|
pub on_run_job: Callback<(String, String)>, // (runner, payload)
|
||||||
pub on_stop_job: Callback<String>,
|
pub on_stop_job: Callback<String>,
|
||||||
pub on_delete_job: Callback<String>,
|
pub on_delete_job: Callback<String>,
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,6 @@ impl PartialEq for JobsProps {
|
|||||||
self.job_form.payload == other.job_form.payload &&
|
self.job_form.payload == other.job_form.payload &&
|
||||||
self.job_form.runner == other.job_form.runner &&
|
self.job_form.runner == other.job_form.runner &&
|
||||||
self.job_form.executor == other.job_form.executor &&
|
self.job_form.executor == other.job_form.executor &&
|
||||||
self.job_form.secret == other.job_form.secret &&
|
|
||||||
self.runners.len() == other.runners.len()
|
self.runners.len() == other.runners.len()
|
||||||
// Note: Callbacks don't implement PartialEq, so we skip them
|
// Note: Callbacks don't implement PartialEq, so we skip them
|
||||||
}
|
}
|
||||||
@@ -57,18 +56,12 @@ pub fn jobs(props: &JobsProps) -> Html {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let on_secret_change = {
|
|
||||||
let on_change = props.on_job_form_change.clone();
|
|
||||||
Callback::from(move |e: Event| {
|
|
||||||
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
||||||
on_change.emit(("secret".to_string(), input.value()));
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_run_click = {
|
let on_run_click = {
|
||||||
let on_run = props.on_run_job.clone();
|
let on_run = props.on_run_job.clone();
|
||||||
|
let job_form = props.job_form.clone();
|
||||||
Callback::from(move |_: MouseEvent| {
|
Callback::from(move |_: MouseEvent| {
|
||||||
on_run.emit(());
|
on_run.emit((job_form.runner.clone(), job_form.payload.clone()));
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,6 +77,7 @@ pub fn jobs(props: &JobsProps) -> Html {
|
|||||||
<th>{"Runner"}</th>
|
<th>{"Runner"}</th>
|
||||||
<th>{"Executor"}</th>
|
<th>{"Executor"}</th>
|
||||||
<th>{"Status"}</th>
|
<th>{"Status"}</th>
|
||||||
|
<th>{"Actions"}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -126,14 +120,10 @@ pub fn jobs(props: &JobsProps) -> Html {
|
|||||||
onchange={on_executor_change}
|
onchange={on_executor_change}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge status-not-started">{"Not Started"}</span>
|
||||||
|
</td>
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
class="form-control table-input secret-input"
|
|
||||||
placeholder="Secret"
|
|
||||||
value={props.job_form.secret.clone()}
|
|
||||||
onchange={on_secret_change}
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
onclick={on_run_click}
|
onclick={on_run_click}
|
||||||
@@ -157,8 +147,10 @@ pub fn jobs(props: &JobsProps) -> Html {
|
|||||||
<td><code class="code">{job.payload()}</code></td>
|
<td><code class="code">{job.payload()}</code></td>
|
||||||
<td>{job.runner()}</td>
|
<td>{job.runner()}</td>
|
||||||
<td>{job.executor()}</td>
|
<td>{job.executor()}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge status-pending">{"Pending"}</span>
|
||||||
|
</td>
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
<span class="status-badge">{"Queued"}</span>
|
|
||||||
<button
|
<button
|
||||||
class="btn-icon btn-stop"
|
class="btn-icon btn-stop"
|
||||||
title="Stop Job"
|
title="Stop Job"
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
mod app;
|
pub mod app;
|
||||||
mod sidebar;
|
pub mod sidebar;
|
||||||
mod runners;
|
pub mod runners;
|
||||||
mod jobs;
|
pub mod jobs;
|
||||||
|
pub mod toast;
|
||||||
|
|
||||||
#[wasm_bindgen(start)]
|
#[wasm_bindgen(start)]
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
|
@@ -18,11 +18,12 @@ pub struct RunnersProps {
|
|||||||
pub runners: Vec<(String, String)>, // (name, status)
|
pub runners: Vec<(String, String)>, // (name, status)
|
||||||
pub register_form: RegisterForm,
|
pub register_form: RegisterForm,
|
||||||
pub ping_states: HashMap<String, PingState>, // runner -> ping_state
|
pub ping_states: HashMap<String, PingState>, // runner -> ping_state
|
||||||
|
pub session_secret: String,
|
||||||
pub on_register_form_change: Callback<(String, String)>,
|
pub on_register_form_change: Callback<(String, String)>,
|
||||||
pub on_register_runner: Callback<()>,
|
pub on_register_runner: Callback<()>,
|
||||||
pub on_load_runners: Callback<()>,
|
pub on_load_runners: Callback<()>,
|
||||||
pub on_remove_runner: Callback<String>,
|
pub on_remove_runner: Callback<String>,
|
||||||
pub on_ping_runner: Callback<(String, String)>, // (runner, secret)
|
pub on_run_job: Callback<(String, String)>, // (runner, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(Runners)]
|
#[function_component(Runners)]
|
||||||
@@ -68,152 +69,137 @@ pub fn runners(props: &RunnersProps) -> Html {
|
|||||||
};
|
};
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="runners-grid">
|
<div class="mb-5">
|
||||||
// Registration card (first card)
|
<h2 class="mb-4">{"Runners"}</h2>
|
||||||
<div class="card register-card">
|
|
||||||
<div class="card-title">{"+ Register Runner"}</div>
|
|
||||||
<form onsubmit={on_register_runner.reform(|e: web_sys::SubmitEvent| {
|
|
||||||
e.prevent_default();
|
|
||||||
()
|
|
||||||
})}>
|
|
||||||
<div class="form-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="Runner name"
|
|
||||||
value={props.register_form.name.clone()}
|
|
||||||
onchange={props.on_register_form_change.reform(|e: web_sys::Event| {
|
|
||||||
let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
|
|
||||||
("name".to_string(), input.value())
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group form-row">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
class="form-control form-control-inline"
|
|
||||||
placeholder="Secret"
|
|
||||||
value={props.register_form.secret.clone()}
|
|
||||||
onchange={props.on_register_form_change.reform(|e: web_sys::Event| {
|
|
||||||
let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
|
|
||||||
("secret".to_string(), input.value())
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
{"Register"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Existing runner cards
|
// All cards in same row - registration card first, then runner cards
|
||||||
{for props.runners.iter().map(|(name, status)| {
|
<div class="d-flex flex-column gap-3">
|
||||||
let status_class = match status.as_str() {
|
// Registration card as first item
|
||||||
"Running" => "status-running",
|
<div class="card bg-dark border-secondary">
|
||||||
"Stopped" => "status-stopped",
|
<div class="card-header bg-transparent border-secondary">
|
||||||
"Starting" => "status-starting",
|
<div class="d-flex align-items-center">
|
||||||
"Stopping" => "status-starting",
|
<i class="fas fa-plus-circle me-2 text-success"></i>
|
||||||
"Registering" => "status-registering",
|
<h6 class="mb-0 text-white">{"Add New Runner"}</h6>
|
||||||
_ => "status-stopped",
|
|
||||||
};
|
|
||||||
|
|
||||||
let name_clone = name.clone();
|
|
||||||
let name_clone2 = name.clone();
|
|
||||||
let on_remove = props.on_remove_runner.clone();
|
|
||||||
let on_ping = props.on_ping_runner.clone();
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<div class="card runner-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="runner-title-section">
|
|
||||||
<div class="runner-title-with-dot">
|
|
||||||
<span class={format!("connection-dot {}", status_class)} title={status.clone()}>
|
|
||||||
{"●"}
|
|
||||||
</span>
|
|
||||||
<div class="card-title">{name}</div>
|
|
||||||
</div>
|
|
||||||
<small class="queue-info">
|
|
||||||
{"redis://localhost:6379/runner:"}{name}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="runner-actions-top">
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-secondary btn-remove"
|
|
||||||
title="Remove Runner"
|
|
||||||
onclick={Callback::from(move |_| on_remove.emit(name_clone2.clone()))}
|
|
||||||
>
|
|
||||||
<svg class="trash-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<polyline points="3,6 5,6 21,6"></polyline>
|
|
||||||
<path d="m5,6 1,14 c0,1.1 0.9,2 2,2 h8 c1.1,0 2,-0.9 2,-2 l1,-14"></path>
|
|
||||||
<path d="m10,11 v6"></path>
|
|
||||||
<path d="m14,11 v6"></path>
|
|
||||||
<path d="M7,6V4c0-1.1,0.9-2,2-2h6c0-1.1,0.9-2,2-2v2"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="runner-chart">
|
|
||||||
<div class="chart-placeholder">
|
|
||||||
{"📊 Live job count chart (5s updates)"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ping-section">
|
|
||||||
{
|
|
||||||
match props.ping_states.get(name).cloned().unwrap_or(PingState::Idle) {
|
|
||||||
PingState::Idle => html! {
|
|
||||||
<div class="input-group input-group-sm">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="Secret"
|
|
||||||
id={format!("ping-secret-{}", name)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-primary"
|
|
||||||
title="Ping Runner"
|
|
||||||
onclick={Callback::from(move |_| {
|
|
||||||
let window = web_sys::window().unwrap();
|
|
||||||
let document = window.document().unwrap();
|
|
||||||
let input_id = format!("ping-secret-{}", name_clone.clone());
|
|
||||||
if let Some(input) = document.get_element_by_id(&input_id) {
|
|
||||||
let input: web_sys::HtmlInputElement = input.dyn_into().unwrap();
|
|
||||||
let secret = input.value();
|
|
||||||
if !secret.is_empty() {
|
|
||||||
on_ping.emit((name_clone.clone(), secret));
|
|
||||||
input.set_value("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{"Ping"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
PingState::Waiting => html! {
|
|
||||||
<div class="ping-status ping-waiting">
|
|
||||||
<span class="ping-spinner">{"⏳"}</span>
|
|
||||||
<span>{"Waiting for response..."}</span>
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
PingState::Success(result) => html! {
|
|
||||||
<div class="ping-status ping-success">
|
|
||||||
<span class="ping-icon">{"✅"}</span>
|
|
||||||
<span>{format!("Success: {}", result)}</span>
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
PingState::Error(error) => html! {
|
|
||||||
<div class="ping-status ping-error">
|
|
||||||
<span class="ping-icon">{"❌"}</span>
|
|
||||||
<span>{error}</span>
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<div class="card-body">
|
||||||
})}
|
<form onsubmit={on_register_runner.reform(|e: SubmitEvent| e.prevent_default())}>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted small">{"Runner Name"}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control bg-secondary border-0 text-white"
|
||||||
|
placeholder="e.g., worker-01"
|
||||||
|
value={props.register_form.name.clone()}
|
||||||
|
oninput={props.on_register_form_change.reform(|e: InputEvent| {
|
||||||
|
let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
|
||||||
|
("name".to_string(), input.value())
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted small">{"Registration Secret"}</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control bg-secondary border-0 text-white"
|
||||||
|
placeholder="Enter secret key"
|
||||||
|
value={props.register_form.secret.clone()}
|
||||||
|
oninput={props.on_register_form_change.reform(|e: InputEvent| {
|
||||||
|
let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
|
||||||
|
("secret".to_string(), input.value())
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success w-100">
|
||||||
|
<i class="fas fa-plus me-1"></i>
|
||||||
|
{"Register Runner"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{for props.runners.iter().map(|(name, status)| {
|
||||||
|
let badge_class = match status.as_str() {
|
||||||
|
"Running" => "status-running",
|
||||||
|
"Stopped" => "status-stopped",
|
||||||
|
"Starting" => "status-starting",
|
||||||
|
"Stopping" => "status-starting",
|
||||||
|
"Registering" => "status-registering",
|
||||||
|
_ => "bg-secondary",
|
||||||
|
};
|
||||||
|
|
||||||
|
let name_clone = name.clone();
|
||||||
|
let name_clone2 = name.clone();
|
||||||
|
let on_remove = props.on_remove_runner.clone();
|
||||||
|
let on_run = props.on_run_job.clone();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="card bg-dark border-secondary mb-3">
|
||||||
|
<div class="card-body d-flex align-items-center justify-content-between p-3">
|
||||||
|
<div class="d-flex align-items-center flex-grow-1">
|
||||||
|
<div class="me-3">
|
||||||
|
<span class={classes!("badge", "rounded-pill", badge_class)}>{"●"}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="text-white mb-1">{name}</h6>
|
||||||
|
<small class="text-muted">{format!("Queue: {}", name)}</small>
|
||||||
|
</div>
|
||||||
|
<div class="me-3">
|
||||||
|
<span class={classes!("badge", badge_class)}>
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
title="Run Job"
|
||||||
|
onclick={Callback::from(move |_| on_run.emit((name_clone.clone(), "test".to_string())))}
|
||||||
|
>
|
||||||
|
<i class="fas fa-play"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
title="Remove Runner"
|
||||||
|
onclick={Callback::from(move |_| on_remove.emit(name_clone2.clone()))}
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ms-3">
|
||||||
|
{
|
||||||
|
match props.ping_states.get(name).cloned().unwrap_or(PingState::Idle) {
|
||||||
|
PingState::Idle => html! {
|
||||||
|
<small class="text-muted">{"Ready"}</small>
|
||||||
|
},
|
||||||
|
PingState::Waiting => html! {
|
||||||
|
<small class="text-info">
|
||||||
|
<i class="fas fa-spinner fa-spin me-1"></i>
|
||||||
|
{"Working..."}
|
||||||
|
</small>
|
||||||
|
},
|
||||||
|
PingState::Success(ref msg) => html! {
|
||||||
|
<small class="text-success">
|
||||||
|
<i class="fas fa-check me-1"></i>
|
||||||
|
{msg}
|
||||||
|
</small>
|
||||||
|
},
|
||||||
|
PingState::Error(ref msg) => html! {
|
||||||
|
<small class="text-danger">
|
||||||
|
<i class="fas fa-times me-1"></i>
|
||||||
|
{msg}
|
||||||
|
</small>
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,24 +2,44 @@ use yew::prelude::*;
|
|||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
use gloo::console;
|
use gloo::console;
|
||||||
use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob};
|
use gloo::storage::{LocalStorage, Storage};
|
||||||
|
use hero_supervisor_openrpc_client::wasm::WasmSupervisorClient;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct SupervisorInfo {
|
pub struct SupervisorInfo {
|
||||||
pub server_url: String,
|
pub server_url: String,
|
||||||
pub admin_secrets_count: usize,
|
pub admin_secrets: Vec<String>,
|
||||||
pub user_secrets_count: usize,
|
pub user_secrets: Vec<String>,
|
||||||
pub register_secrets_count: usize,
|
pub register_secrets: Vec<String>,
|
||||||
pub runners_count: usize,
|
pub runners_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
pub enum SessionSecretType {
|
pub enum SessionSecretType {
|
||||||
None,
|
None,
|
||||||
User,
|
User,
|
||||||
Admin,
|
Admin,
|
||||||
|
Register,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct SessionData {
|
||||||
|
pub secret: String,
|
||||||
|
pub secret_type: SessionSecretType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
|
pub struct StoredSecrets {
|
||||||
|
pub admin_secrets: Vec<String>,
|
||||||
|
pub user_secrets: Vec<String>,
|
||||||
|
pub register_secrets: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const STORED_SECRETS_KEY: &str = "supervisor_stored_secrets";
|
||||||
|
|
||||||
|
pub const SESSION_STORAGE_KEY: &str = "supervisor_session";
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct SidebarProps {
|
pub struct SidebarProps {
|
||||||
pub server_url: String,
|
pub server_url: String,
|
||||||
@@ -28,17 +48,27 @@ pub struct SidebarProps {
|
|||||||
pub session_secret_type: SessionSecretType,
|
pub session_secret_type: SessionSecretType,
|
||||||
pub on_session_secret_change: Callback<(String, SessionSecretType)>,
|
pub on_session_secret_change: Callback<(String, SessionSecretType)>,
|
||||||
pub on_supervisor_info_loaded: Callback<SupervisorInfo>,
|
pub on_supervisor_info_loaded: Callback<SupervisorInfo>,
|
||||||
|
pub on_add_secret: Callback<(SessionSecretType, String)>,
|
||||||
|
pub on_remove_secret: Callback<(SessionSecretType, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(Sidebar)]
|
#[function_component(Sidebar)]
|
||||||
pub fn sidebar(props: &SidebarProps) -> Html {
|
pub fn sidebar(props: &SidebarProps) -> Html {
|
||||||
let session_secret_input = use_state(|| String::new());
|
let session_secret_input = use_state(|| String::new());
|
||||||
let payload_input = use_state(|| String::new());
|
let selected_secret_type = use_state(|| SessionSecretType::Admin);
|
||||||
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 is_loading = use_state(|| false);
|
||||||
|
|
||||||
|
// Load session from localStorage on component mount
|
||||||
|
{
|
||||||
|
let on_session_secret_change = props.on_session_secret_change.clone();
|
||||||
|
use_effect_with((), move |_| {
|
||||||
|
if let Ok(session_data) = LocalStorage::get::<SessionData>(SESSION_STORAGE_KEY) {
|
||||||
|
on_session_secret_change.emit((session_data.secret, session_data.secret_type));
|
||||||
|
}
|
||||||
|
|| ()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let on_session_secret_change = {
|
let on_session_secret_change = {
|
||||||
let session_secret_input = session_secret_input.clone();
|
let session_secret_input = session_secret_input.clone();
|
||||||
Callback::from(move |e: web_sys::Event| {
|
Callback::from(move |e: web_sys::Event| {
|
||||||
@@ -49,12 +79,11 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
|||||||
|
|
||||||
let on_session_secret_submit = {
|
let on_session_secret_submit = {
|
||||||
let session_secret_input = session_secret_input.clone();
|
let session_secret_input = session_secret_input.clone();
|
||||||
|
let selected_secret_type = selected_secret_type.clone();
|
||||||
let is_loading = is_loading.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();
|
|
||||||
let on_session_secret_change = props.on_session_secret_change.clone();
|
let on_session_secret_change = props.on_session_secret_change.clone();
|
||||||
|
let on_supervisor_info_loaded = props.on_supervisor_info_loaded.clone();
|
||||||
|
let server_url = props.server_url.clone();
|
||||||
|
|
||||||
Callback::from(move |_: web_sys::MouseEvent| {
|
Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
let secret = (*session_secret_input).clone();
|
let secret = (*session_secret_input).clone();
|
||||||
@@ -63,346 +92,431 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is_loading.set(true);
|
is_loading.set(true);
|
||||||
let client = WasmSupervisorClient::new(server_url.clone());
|
|
||||||
|
|
||||||
let session_secret_input = session_secret_input.clone();
|
|
||||||
let is_loading = is_loading.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 on_session_secret_change = on_session_secret_change.clone();
|
let on_session_secret_change = on_session_secret_change.clone();
|
||||||
|
let on_supervisor_info_loaded = on_supervisor_info_loaded.clone();
|
||||||
|
let server_url = server_url.clone();
|
||||||
|
let session_secret_input = session_secret_input.clone();
|
||||||
|
let selected_secret_type = selected_secret_type.clone();
|
||||||
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
// Try to get admin secrets first to determine if this is an admin secret
|
let client = WasmSupervisorClient::new(server_url.clone());
|
||||||
match client.list_admin_secrets(&secret).await {
|
|
||||||
Ok(admin_secret_list) => {
|
match client.discover().await {
|
||||||
// This is an admin secret
|
Ok(_) => {
|
||||||
admin_secrets.set(admin_secret_list);
|
console::log!("Connected to supervisor successfully");
|
||||||
|
|
||||||
// Also load user and register secrets
|
let secret_type = (*selected_secret_type).clone();
|
||||||
if let Ok(user_secret_list) = client.list_user_secrets(&secret).await {
|
|
||||||
user_secrets.set(user_secret_list);
|
|
||||||
}
|
|
||||||
if let Ok(register_secret_list) = client.list_register_secrets(&secret).await {
|
|
||||||
register_secrets.set(register_secret_list);
|
|
||||||
}
|
|
||||||
|
|
||||||
on_session_secret_change.emit((secret, SessionSecretType::Admin));
|
// Don't store secrets in localStorage - use API only
|
||||||
console::log!("Admin session established");
|
|
||||||
}
|
// Save to localStorage
|
||||||
Err(_) => {
|
let session_data = SessionData {
|
||||||
// Try as user secret - just test if we can make any call with it
|
secret: secret.clone(),
|
||||||
match client.list_runners().await {
|
secret_type: secret_type.clone(),
|
||||||
Ok(_) => {
|
};
|
||||||
// This appears to be a valid user secret
|
let _ = LocalStorage::set(SESSION_STORAGE_KEY, &session_data);
|
||||||
on_session_secret_change.emit((secret, SessionSecretType::User));
|
|
||||||
console::log!("User session established");
|
// Create supervisor info with empty secrets initially
|
||||||
|
let mut supervisor_info = SupervisorInfo {
|
||||||
|
server_url: server_url.clone(),
|
||||||
|
admin_secrets: vec![],
|
||||||
|
user_secrets: vec![],
|
||||||
|
register_secrets: vec![],
|
||||||
|
runners_count: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only fetch secrets if this is an admin secret
|
||||||
|
if secret_type == SessionSecretType::Admin {
|
||||||
|
console::log!("Attempting to fetch secrets with admin secret");
|
||||||
|
|
||||||
|
// Try to fetch admin secrets
|
||||||
|
match client.list_admin_secrets(&secret).await {
|
||||||
|
Ok(admin_secrets) => {
|
||||||
|
console::log!("✅ Fetched admin secrets:", format!("{:?}", admin_secrets));
|
||||||
|
supervisor_info.admin_secrets = admin_secrets;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
console::error!("❌ Failed to fetch admin secrets:", format!("{:?}", e));
|
||||||
|
// If admin secret fetch fails, this might not be a valid admin secret
|
||||||
|
supervisor_info.admin_secrets = vec![secret.clone()];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
console::log!("Invalid secret:", format!("{:?}", e));
|
// Try to fetch user secrets
|
||||||
on_session_secret_change.emit((String::new(), SessionSecretType::None));
|
match client.list_user_secrets(&secret).await {
|
||||||
|
Ok(user_secrets) => {
|
||||||
|
console::log!("✅ Fetched user secrets:", format!("{:?}", user_secrets));
|
||||||
|
supervisor_info.user_secrets = user_secrets;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
console::error!("❌ Failed to fetch user secrets:", format!("{:?}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fetch register secrets
|
||||||
|
match client.list_register_secrets(&secret).await {
|
||||||
|
Ok(register_secrets) => {
|
||||||
|
console::log!("✅ Fetched register secrets:", format!("{:?}", register_secrets));
|
||||||
|
supervisor_info.register_secrets = register_secrets;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
console::error!("❌ Failed to fetch register secrets:", format!("{:?}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console::log!("Non-admin secret - showing only current secret");
|
||||||
|
// For non-admin secrets, only show the current secret
|
||||||
|
match secret_type {
|
||||||
|
SessionSecretType::User => {
|
||||||
|
supervisor_info.user_secrets = vec![secret.clone()];
|
||||||
|
},
|
||||||
|
SessionSecretType::Register => {
|
||||||
|
supervisor_info.register_secrets = vec![secret.clone()];
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
on_session_secret_change.emit((secret.clone(), secret_type.clone()));
|
||||||
|
on_supervisor_info_loaded.emit(supervisor_info);
|
||||||
is_loading.set(false);
|
session_secret_input.set(String::new());
|
||||||
session_secret_input.set(String::new());
|
|
||||||
});
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_session_clear = {
|
|
||||||
let on_session_secret_change = props.on_session_secret_change.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| {
|
|
||||||
on_session_secret_change.emit((String::new(), SessionSecretType::None));
|
|
||||||
admin_secrets.set(Vec::new());
|
|
||||||
user_secrets.set(Vec::new());
|
|
||||||
register_secrets.set(Vec::new());
|
|
||||||
console::log!("Session cleared");
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_payload_change = {
|
|
||||||
let payload_input = payload_input.clone();
|
|
||||||
Callback::from(move |e: web_sys::Event| {
|
|
||||||
let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
|
|
||||||
payload_input.set(input.value());
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_run_click = {
|
|
||||||
let payload_input = payload_input.clone();
|
|
||||||
let server_url = props.server_url.clone();
|
|
||||||
let session_secret = props.session_secret.clone();
|
|
||||||
let is_loading = is_loading.clone();
|
|
||||||
|
|
||||||
Callback::from(move |_: web_sys::MouseEvent| {
|
|
||||||
let payload = (*payload_input).clone();
|
|
||||||
if payload.is_empty() || session_secret.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
is_loading.set(true);
|
|
||||||
let client = WasmSupervisorClient::new(server_url.clone());
|
|
||||||
|
|
||||||
let payload_input = payload_input.clone();
|
|
||||||
let is_loading = is_loading.clone();
|
|
||||||
let session_secret = session_secret.clone();
|
|
||||||
|
|
||||||
spawn_local(async move {
|
|
||||||
// Create WasmJob object using constructor
|
|
||||||
let job = WasmJob::new(
|
|
||||||
uuid::Uuid::new_v4().to_string(),
|
|
||||||
payload.clone(),
|
|
||||||
"osis".to_string(),
|
|
||||||
"default".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
match client.create_job(session_secret.clone(), job).await {
|
|
||||||
Ok(job_id) => {
|
|
||||||
console::log!("Job created successfully:", job_id);
|
|
||||||
payload_input.set(String::new());
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
console::log!("Failed to create job:", format!("{:?}", e));
|
console::error!("Failed to connect to supervisor:", format!("{:?}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is_loading.set(false);
|
is_loading.set(false);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_logout = {
|
||||||
|
let on_session_secret_change = props.on_session_secret_change.clone();
|
||||||
|
Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
|
// Clear localStorage
|
||||||
|
let _ = LocalStorage::delete(SESSION_STORAGE_KEY);
|
||||||
|
on_session_secret_change.emit((String::new(), SessionSecretType::None));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_add_admin_secret = {
|
||||||
|
let on_add_secret = props.on_add_secret.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
on_add_secret.emit((SessionSecretType::Admin, String::new()));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_add_user_secret = {
|
||||||
|
let on_add_secret = props.on_add_secret.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
on_add_secret.emit((SessionSecretType::User, String::new()));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_add_register_secret = {
|
||||||
|
let on_add_secret = props.on_add_secret.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
on_add_secret.emit((SessionSecretType::User, String::new()));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_session_input = {
|
||||||
|
let session_secret_input = session_secret_input.clone();
|
||||||
|
Callback::from(move |e: InputEvent| {
|
||||||
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||||
|
session_secret_input.set(input.value());
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_session_keypress = {
|
||||||
|
let on_session_secret_submit = on_session_secret_submit.clone();
|
||||||
|
Callback::from(move |e: KeyboardEvent| {
|
||||||
|
if e.key() == "Enter" {
|
||||||
|
e.prevent_default();
|
||||||
|
// Create a dummy MouseEvent to trigger the submit handler
|
||||||
|
let dummy_event = web_sys::MouseEvent::new("click").unwrap();
|
||||||
|
on_session_secret_submit.emit(dummy_event);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_session_toggle = {
|
||||||
|
let on_session_secret_submit = on_session_secret_submit.clone();
|
||||||
|
let on_session_secret_change = props.on_session_secret_change.clone();
|
||||||
|
let session_secret = props.session_secret.clone();
|
||||||
|
Callback::from(move |e: web_sys::MouseEvent| {
|
||||||
|
if session_secret.is_empty() {
|
||||||
|
// Try to login with current input
|
||||||
|
on_session_secret_submit.emit(e);
|
||||||
|
} else {
|
||||||
|
// Logout - clear localStorage and session
|
||||||
|
let _ = LocalStorage::delete(SESSION_STORAGE_KEY);
|
||||||
|
on_session_secret_change.emit((String::new(), SessionSecretType::None));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to refresh secrets from API when switching to an admin secret
|
||||||
|
let refresh_secrets_from_api = {
|
||||||
|
let server_url = props.server_url.clone();
|
||||||
|
let on_supervisor_info_loaded = props.on_supervisor_info_loaded.clone();
|
||||||
|
Callback::from(move |(secret, secret_type): (String, SessionSecretType)| {
|
||||||
|
if secret_type == SessionSecretType::Admin {
|
||||||
|
let client = WasmSupervisorClient::new(server_url.clone());
|
||||||
|
let on_supervisor_info_loaded = on_supervisor_info_loaded.clone();
|
||||||
|
let server_url = server_url.clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
let mut supervisor_info = SupervisorInfo {
|
||||||
|
server_url: server_url.clone(),
|
||||||
|
admin_secrets: vec![],
|
||||||
|
user_secrets: vec![],
|
||||||
|
register_secrets: vec![],
|
||||||
|
runners_count: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch real secrets from the API
|
||||||
|
match client.list_admin_secrets(&secret).await {
|
||||||
|
Ok(admin_secrets) => {
|
||||||
|
console::log!("Refreshed admin secrets from API:", format!("{:?}", admin_secrets));
|
||||||
|
supervisor_info.admin_secrets = admin_secrets;
|
||||||
|
},
|
||||||
|
Err(e) => console::error!("Failed to refresh admin secrets:", format!("{:?}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
match client.list_user_secrets(&secret).await {
|
||||||
|
Ok(user_secrets) => {
|
||||||
|
console::log!("Refreshed user secrets from API:", format!("{:?}", user_secrets));
|
||||||
|
supervisor_info.user_secrets = user_secrets;
|
||||||
|
},
|
||||||
|
Err(e) => console::error!("Failed to refresh user secrets:", format!("{:?}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
match client.list_register_secrets(&secret).await {
|
||||||
|
Ok(register_secrets) => {
|
||||||
|
console::log!("Refreshed register secrets from API:", format!("{:?}", register_secrets));
|
||||||
|
supervisor_info.register_secrets = register_secrets;
|
||||||
|
},
|
||||||
|
Err(e) => console::error!("Failed to refresh register secrets:", format!("{:?}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
on_supervisor_info_loaded.emit(supervisor_info);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="sidebar">
|
<div class="col-md-3 col-lg-2 d-md-block sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="bg-dark rounded m-2 h-100 d-flex flex-column p-3 sidebar-island">
|
||||||
<h2>{"Supervisor"}</h2>
|
// Header section
|
||||||
</div>
|
<div class="pb-3 border-bottom border-secondary">
|
||||||
<div class="sidebar-content">
|
<h5 class="text-white mb-1">{"Supervisor"}</h5>
|
||||||
<div class="sidebar-sections">
|
<small class="text-muted">{"Admin interface for managing jobs and secrets"}</small>
|
||||||
// Server Info Section
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
// Session Secret Management Section
|
// Session login section
|
||||||
<div class="session-section">
|
<div class="py-3 border-bottom border-secondary">
|
||||||
<div class="session-header">
|
<div class="mb-2">
|
||||||
<span class="session-title">{"Session"}</span>
|
<select
|
||||||
{
|
class="form-select form-select-sm bg-secondary text-white border-0"
|
||||||
match props.session_secret_type {
|
onchange={{
|
||||||
SessionSecretType::Admin => html! {
|
let selected_secret_type = selected_secret_type.clone();
|
||||||
<span class="session-badge admin">{"Admin"}</span>
|
Callback::from(move |e: Event| {
|
||||||
},
|
let select: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||||
SessionSecretType::User => html! {
|
let secret_type = match select.value().as_str() {
|
||||||
<span class="session-badge user">{"User"}</span>
|
"Admin" => SessionSecretType::Admin,
|
||||||
},
|
"User" => SessionSecretType::User,
|
||||||
SessionSecretType::None => html! {
|
"Register" => SessionSecretType::Register,
|
||||||
<span class="session-badge none">{"None"}</span>
|
_ => SessionSecretType::Admin,
|
||||||
}
|
};
|
||||||
}
|
selected_secret_type.set(secret_type);
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="Admin" selected={*selected_secret_type == SessionSecretType::Admin}>{"Admin Secret"}</option>
|
||||||
|
<option value="User" selected={*selected_secret_type == SessionSecretType::User}>{"User Secret"}</option>
|
||||||
|
<option value="Register" selected={*selected_secret_type == SessionSecretType::Register}>{"Register Secret"}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control bg-secondary text-white border-0"
|
||||||
|
placeholder={format!("Enter {} secret...", match *selected_secret_type {
|
||||||
|
SessionSecretType::Admin => "admin",
|
||||||
|
SessionSecretType::User => "user",
|
||||||
|
SessionSecretType::Register => "register",
|
||||||
|
_ => "session"
|
||||||
|
})}
|
||||||
|
value={(*session_secret_input).clone()}
|
||||||
|
oninput={on_session_input}
|
||||||
|
onkeypress={on_session_keypress}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class={classes!("btn", if props.session_secret.is_empty() { "btn-outline-secondary" } else { "btn-outline-success" })}
|
||||||
|
onclick={on_session_toggle}
|
||||||
|
>
|
||||||
|
if props.session_secret.is_empty() {
|
||||||
|
{"🔒"}
|
||||||
|
} else {
|
||||||
|
{"🔓"}
|
||||||
}
|
}
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
if props.session_secret_type == SessionSecretType::None {
|
</div>
|
||||||
<div class="session-input-row">
|
|
||||||
<input
|
// Secret management section (only show when logged in)
|
||||||
type="password"
|
if !props.session_secret.is_empty() {
|
||||||
class="session-input"
|
<div class="flex-grow-1 overflow-auto">
|
||||||
placeholder="Enter secret to establish session"
|
<div class="py-3">
|
||||||
value={(*session_secret_input).clone()}
|
<h6 class="text-white text-uppercase fw-bold mb-3">{"Secret Management"}</h6>
|
||||||
onchange={on_session_secret_change}
|
|
||||||
disabled={*is_loading}
|
// Admin Secrets
|
||||||
/>
|
<div class="mb-4">
|
||||||
<button
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
class="session-btn"
|
<small class="text-muted text-uppercase fw-bold">{"Admin Secrets"}</small>
|
||||||
onclick={on_session_secret_submit}
|
<button class="btn btn-sm btn-outline-secondary border-0" onclick={on_add_admin_secret.clone()}>
|
||||||
disabled={*is_loading || session_secret_input.is_empty()}
|
{"➕"}
|
||||||
>
|
|
||||||
if *is_loading {
|
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
|
||||||
} else {
|
|
||||||
<i class="fas fa-sign-in-alt"></i>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="session-active">
|
|
||||||
<div class="session-info">
|
|
||||||
<span class="session-secret-preview">
|
|
||||||
{format!("{}...", &props.session_secret[..std::cmp::min(8, props.session_secret.len())])}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="session-clear-btn"
|
|
||||||
onclick={on_session_clear}
|
|
||||||
>
|
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="list-group list-group-flush">
|
||||||
}
|
{for props.supervisor_info.as_ref().map(|info| &info.admin_secrets).unwrap_or(&Vec::new()).iter().map(|secret| {
|
||||||
</div>
|
let is_current = secret == &props.session_secret && props.session_secret_type == SessionSecretType::Admin;
|
||||||
|
let on_select = {
|
||||||
// Secrets Management Section (only visible for admin)
|
let on_change = props.on_session_secret_change.clone();
|
||||||
if props.session_secret_type == SessionSecretType::Admin {
|
let refresh_secrets = refresh_secrets_from_api.clone();
|
||||||
<div class="secrets-section">
|
let secret = secret.clone();
|
||||||
<div class="secrets-header">
|
Callback::from(move |_| {
|
||||||
<span class="secrets-title">{"Secrets Management"}</span>
|
on_change.emit((secret.clone(), SessionSecretType::Admin));
|
||||||
|
refresh_secrets.emit((secret.clone(), SessionSecretType::Admin));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let on_remove = {
|
||||||
|
let on_remove = props.on_remove_secret.clone();
|
||||||
|
let secret = secret.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
on_remove.emit((SessionSecretType::Admin, secret.clone()));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<div class={classes!("list-group-item", "d-flex", "justify-content-between", "align-items-center", "bg-secondary", "border-0", "mb-1", if is_current { "border-success" } else { "" })}>
|
||||||
|
<code class={classes!("text-white", "small", "cursor-pointer", if is_current { "text-success" } else { "" })} onclick={on_select}>
|
||||||
|
{format!("{}...{}", &secret[..4.min(secret.len())], if secret.len() > 8 { &secret[secret.len()-4..] } else { "" })}
|
||||||
|
</code>
|
||||||
|
<button class="btn btn-sm text-danger border-0 p-0" onclick={on_remove}>
|
||||||
|
{"❌"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="secrets-content">
|
// User Secrets
|
||||||
<div class="secret-group">
|
<div class="mb-4">
|
||||||
<div class="secret-header">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<span class="secret-title">{"Admin secrets"}</span>
|
<small class="text-muted text-uppercase fw-bold">{"User Secrets"}</small>
|
||||||
<span class="secret-count">{admin_secrets.len()}</span>
|
<button class="btn btn-sm btn-outline-secondary border-0" onclick={on_add_user_secret.clone()}>
|
||||||
</div>
|
{"➕"}
|
||||||
<div class="secret-list">
|
</button>
|
||||||
{ 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
<div class="secret-group">
|
{for props.supervisor_info.as_ref().map(|info| &info.user_secrets).unwrap_or(&Vec::new()).iter().map(|secret| {
|
||||||
<div class="secret-header">
|
let is_current = secret == &props.session_secret && props.session_secret_type == SessionSecretType::User;
|
||||||
<span class="secret-title">{"User secrets"}</span>
|
let on_select = {
|
||||||
<span class="secret-count">{user_secrets.len()}</span>
|
let on_change = props.on_session_secret_change.clone();
|
||||||
</div>
|
let secret = secret.clone();
|
||||||
<div class="secret-list">
|
Callback::from(move |_| {
|
||||||
{ for user_secrets.iter().enumerate().map(|(i, secret)| {
|
on_change.emit((secret.clone(), SessionSecretType::User));
|
||||||
html! {
|
})
|
||||||
<div class="secret-item" key={i}>
|
};
|
||||||
<div class="secret-value">{secret.clone()}</div>
|
let on_remove = {
|
||||||
<button class="btn-icon btn-remove">
|
let on_remove = props.on_remove_secret.clone();
|
||||||
<i class="fas fa-minus"></i>
|
let secret = secret.clone();
|
||||||
</button>
|
Callback::from(move |_| {
|
||||||
</div>
|
on_remove.emit((SessionSecretType::User, secret.clone()));
|
||||||
}
|
})
|
||||||
})}
|
};
|
||||||
</div>
|
html! {
|
||||||
|
<div class={classes!("list-group-item", "d-flex", "justify-content-between", "align-items-center", "bg-secondary", "border-0", "mb-1", if is_current { "border-success" } else { "" })}>
|
||||||
|
<code class={classes!("text-white", "small", "cursor-pointer", if is_current { "text-success" } else { "" })} onclick={on_select}>
|
||||||
|
{format!("{}...{}", &secret[..4.min(secret.len())], if secret.len() > 8 { &secret[secret.len()-4..] } else { "" })}
|
||||||
|
</code>
|
||||||
|
<button class="btn btn-sm text-danger border-0 p-0" onclick={on_remove}>
|
||||||
|
{"❌"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="secret-group">
|
|
||||||
<div class="secret-header">
|
// Register Secrets
|
||||||
<span class="secret-title">{"Register secrets"}</span>
|
<div class="mb-4">
|
||||||
<span class="secret-count">{register_secrets.len()}</span>
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
</div>
|
<small class="text-muted text-uppercase fw-bold">{"Register Secrets"}</small>
|
||||||
<div class="secret-list">
|
<button class="btn btn-sm btn-outline-secondary border-0" onclick={on_add_register_secret.clone()}>
|
||||||
{ for register_secrets.iter().enumerate().map(|(i, secret)| {
|
{"➕"}
|
||||||
html! {
|
</button>
|
||||||
<div class="secret-item" key={i}>
|
</div>
|
||||||
<div class="secret-value">{secret.clone()}</div>
|
<div class="list-group list-group-flush">
|
||||||
<button class="btn-icon btn-remove">
|
{for props.supervisor_info.as_ref().map(|info| &info.register_secrets).unwrap_or(&Vec::new()).iter().map(|secret| {
|
||||||
<i class="fas fa-minus"></i>
|
let is_current = secret == &props.session_secret;
|
||||||
</button>
|
let on_select = {
|
||||||
</div>
|
let on_change = props.on_session_secret_change.clone();
|
||||||
}
|
let secret = secret.clone();
|
||||||
})}
|
Callback::from(move |_| {
|
||||||
</div>
|
on_change.emit((secret.clone(), SessionSecretType::Register));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let on_remove = {
|
||||||
|
let on_remove = props.on_remove_secret.clone();
|
||||||
|
let secret = secret.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
on_remove.emit((SessionSecretType::Register, secret.clone()));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<div class={classes!("list-group-item", "d-flex", "justify-content-between", "align-items-center", "bg-secondary", "border-0", "mb-1", if is_current { "border-success" } else { "" })}>
|
||||||
|
<code class={classes!("text-white", "small", "cursor-pointer", if is_current { "text-success" } else { "" })} onclick={on_select}>
|
||||||
|
{format!("{}...{}", &secret[..4.min(secret.len())], if secret.len() > 8 { &secret[secret.len()-4..] } else { "" })}
|
||||||
|
</code>
|
||||||
|
<button class="btn btn-sm text-danger border-0 p-0" onclick={on_remove}>
|
||||||
|
{"❌"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
// Quick Actions Section
|
// Navigation and status at bottom
|
||||||
<div class="quick-actions">
|
<div class="mt-auto">
|
||||||
<div class="quick-actions-header">
|
// Navigation links
|
||||||
<span class="quick-actions-title">{"Quick Actions"}</span>
|
<div class="py-2 border-top border-secondary">
|
||||||
</div>
|
<div class="nav nav-pills flex-column">
|
||||||
<div class="quick-actions-content">
|
<a href="#runners" class="nav-link text-muted small">{"Runners"}</a>
|
||||||
if props.session_secret_type != SessionSecretType::None {
|
<a href="#jobs" class="nav-link text-muted small">{"Jobs"}</a>
|
||||||
<div class="action-row">
|
<a href="#logs" class="nav-link text-muted small">{"Logs"}</a>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="action-input"
|
|
||||||
placeholder="Enter payload for job"
|
|
||||||
value={(*payload_input).clone()}
|
|
||||||
onchange={on_payload_change}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="action-btn run-btn"
|
|
||||||
onclick={on_run_click}
|
|
||||||
disabled={payload_input.is_empty() || *is_loading}
|
|
||||||
>
|
|
||||||
if *is_loading {
|
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
|
||||||
} else {
|
|
||||||
<i class="fas fa-play"></i>
|
|
||||||
}
|
|
||||||
{"Run"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="action-disabled">
|
|
||||||
<span>{"Establish a session to enable quick actions"}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
// Supervisor Info Section
|
// Server status
|
||||||
if let Some(info) = &props.supervisor_info {
|
<div class="py-2 border-top border-secondary">
|
||||||
<div class="supervisor-info">
|
<div class="d-flex align-items-center">
|
||||||
<div class="supervisor-info-header">
|
<span class={classes!("badge", "me-2", if props.supervisor_info.is_some() { "bg-success" } else { "bg-danger" })}>
|
||||||
<span class="supervisor-info-title">{"Supervisor Info"}</span>
|
{"●"}
|
||||||
</div>
|
</span>
|
||||||
<div class="supervisor-info-content">
|
<small class="text-muted">
|
||||||
<div class="info-item">
|
{if props.supervisor_info.is_some() { "Connected" } else { "Disconnected" }}
|
||||||
<span class="info-label">{"Admin secrets:"}</span>
|
</small>
|
||||||
<span class="info-value">{info.admin_secrets_count}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">{"User secrets:"}</span>
|
|
||||||
<span class="info-value">{info.user_secrets_count}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">{"Register secrets:"}</span>
|
|
||||||
<span class="info-value">{info.register_secrets_count}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">{"Runners:"}</span>
|
|
||||||
<span class="info-value">{info.runners_count}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
639
clients/admin-ui/src/sidebar_old.rs
Normal file
639
clients/admin-ui/src/sidebar_old.rs
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
use yew::prelude::*;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use gloo::console;
|
||||||
|
use gloo::storage::{LocalStorage, Storage};
|
||||||
|
use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[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(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum SessionSecretType {
|
||||||
|
None,
|
||||||
|
User,
|
||||||
|
Admin,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct SessionData {
|
||||||
|
secret: String,
|
||||||
|
secret_type: SessionSecretType,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SESSION_STORAGE_KEY: &str = "supervisor_session";
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct SidebarProps {
|
||||||
|
pub server_url: String,
|
||||||
|
pub supervisor_info: Option<SupervisorInfo>,
|
||||||
|
pub session_secret: String,
|
||||||
|
pub session_secret_type: SessionSecretType,
|
||||||
|
pub on_session_secret_change: Callback<(String, SessionSecretType)>,
|
||||||
|
pub on_supervisor_info_loaded: Callback<SupervisorInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(Sidebar)]
|
||||||
|
pub fn sidebar(props: &SidebarProps) -> Html {
|
||||||
|
let session_secret_input = use_state(|| String::new());
|
||||||
|
let is_loading = use_state(|| false);
|
||||||
|
|
||||||
|
// Load session from localStorage on component mount
|
||||||
|
{
|
||||||
|
let on_session_secret_change = props.on_session_secret_change.clone();
|
||||||
|
use_effect_with((), move |_| {
|
||||||
|
if let Ok(session_data) = LocalStorage::get::<SessionData>(SESSION_STORAGE_KEY) {
|
||||||
|
on_session_secret_change.emit((session_data.secret, session_data.secret_type));
|
||||||
|
}
|
||||||
|
|| ()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let on_session_secret_change = {
|
||||||
|
let session_secret_input = session_secret_input.clone();
|
||||||
|
Callback::from(move |e: web_sys::Event| {
|
||||||
|
let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
|
||||||
|
session_secret_input.set(input.value());
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_session_secret_submit = {
|
||||||
|
let session_secret_input = session_secret_input.clone();
|
||||||
|
let is_loading = is_loading.clone();
|
||||||
|
let on_session_secret_change = props.on_session_secret_change.clone();
|
||||||
|
let on_supervisor_info_loaded = props.on_supervisor_info_loaded.clone();
|
||||||
|
let server_url = props.server_url.clone();
|
||||||
|
|
||||||
|
Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
|
let secret = (*session_secret_input).clone();
|
||||||
|
if secret.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_loading.set(true);
|
||||||
|
let is_loading = is_loading.clone();
|
||||||
|
let on_session_secret_change = on_session_secret_change.clone();
|
||||||
|
let on_supervisor_info_loaded = on_supervisor_info_loaded.clone();
|
||||||
|
let server_url = server_url.clone();
|
||||||
|
let session_secret_input = session_secret_input.clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
let client = WasmSupervisorClient::new(server_url.clone());
|
||||||
|
|
||||||
|
match client.discover().await {
|
||||||
|
Ok(_) => {
|
||||||
|
console::log!("Connected to supervisor successfully");
|
||||||
|
|
||||||
|
let secret_type = if secret.starts_with("admin_") {
|
||||||
|
SessionSecretType::Admin
|
||||||
|
} else if secret.starts_with("user_") {
|
||||||
|
SessionSecretType::User
|
||||||
|
} else {
|
||||||
|
SessionSecretType::User
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save to localStorage
|
||||||
|
let session_data = SessionData {
|
||||||
|
secret: secret.clone(),
|
||||||
|
secret_type: secret_type.clone(),
|
||||||
|
};
|
||||||
|
let _ = LocalStorage::set(SESSION_STORAGE_KEY, &session_data);
|
||||||
|
|
||||||
|
let supervisor_info = SupervisorInfo {
|
||||||
|
server_url: server_url.clone(),
|
||||||
|
admin_secrets_count: if secret_type == SessionSecretType::Admin { 1 } else { 0 },
|
||||||
|
user_secrets_count: 1,
|
||||||
|
register_secrets_count: 0,
|
||||||
|
runners_count: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
on_session_secret_change.emit((secret.clone(), secret_type.clone()));
|
||||||
|
on_supervisor_info_loaded.emit(supervisor_info);
|
||||||
|
session_secret_input.set(String::new());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
console::error!("Failed to connect to supervisor:", format!("{:?}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is_loading.set(false);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_logout = {
|
||||||
|
let on_session_secret_change = props.on_session_secret_change.clone();
|
||||||
|
Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
|
// Clear localStorage
|
||||||
|
let _ = LocalStorage::delete(SESSION_STORAGE_KEY);
|
||||||
|
on_session_secret_change.emit((String::new(), SessionSecretType::None));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="sidebar">
|
||||||
|
// Header with logo and title
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="logo-section">
|
||||||
|
<div class="logo">{"⚡"}</div>
|
||||||
|
<h1 class="title">{"Supervisor"}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Main control island
|
||||||
|
<div class="control-island">
|
||||||
|
// Session Login Section
|
||||||
|
<div class="session-section">
|
||||||
|
<h3 class="section-title">{"Session Login"}</h3>
|
||||||
|
|
||||||
|
if props.session_secret.is_empty() {
|
||||||
|
<div class="login-form">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="secret-input"
|
||||||
|
placeholder="Enter session secret"
|
||||||
|
value={(*session_secret_input).clone()}
|
||||||
|
onchange={on_session_secret_change}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="connect-btn"
|
||||||
|
onclick={on_session_secret_submit}
|
||||||
|
disabled={*is_loading}
|
||||||
|
>
|
||||||
|
if *is_loading {
|
||||||
|
<span class="loading-spinner"></span>
|
||||||
|
{"Connecting"}
|
||||||
|
} else {
|
||||||
|
{"🔐 Connect"}
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="login-hint">
|
||||||
|
{"Use admin_ or user_ prefixed secrets"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="session-active">
|
||||||
|
<div class="session-status">
|
||||||
|
<div class="status-indicator"></div>
|
||||||
|
<div class="status-info">
|
||||||
|
<span class="status-text">{"Connected"}</span>
|
||||||
|
<span class="session-badge">{
|
||||||
|
match props.session_secret_type {
|
||||||
|
SessionSecretType::Admin => "Admin",
|
||||||
|
SessionSecretType::User => "User",
|
||||||
|
SessionSecretType::None => "None",
|
||||||
|
}
|
||||||
|
}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="logout-btn"
|
||||||
|
onclick={on_logout}
|
||||||
|
>
|
||||||
|
{"🚪 Logout"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Secret Management Section (Admin only)
|
||||||
|
if props.session_secret_type == SessionSecretType::Admin && !props.session_secret.is_empty() {
|
||||||
|
<div class="secrets-section">
|
||||||
|
<h3 class="section-title">{"Secret Management"}</h3>
|
||||||
|
<div class="secret-display">
|
||||||
|
<div class="secret-item">
|
||||||
|
<label class="secret-label">{"Current Session Secret"}</label>
|
||||||
|
<div class="secret-value">
|
||||||
|
<code>{&props.session_secret}</code>
|
||||||
|
<button class="copy-btn" title="Copy to clipboard">{"📋"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server Info Section
|
||||||
|
if let Some(info) = &props.supervisor_info {
|
||||||
|
<div class="server-info-section">
|
||||||
|
<h3 class="section-title">{"Server Status"}</h3>
|
||||||
|
<div class="info-cards">
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-icon">{"🏃"}</div>
|
||||||
|
<div class="info-content">
|
||||||
|
<div class="info-number">{info.runners_count}</div>
|
||||||
|
<div class="info-label">{"Runners"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-icon">{"🔗"}</div>
|
||||||
|
<div class="info-content">
|
||||||
|
<div class="info-text">{&info.server_url.replace("http://", "").replace("https://", "")}</div>
|
||||||
|
<div class="info-label">{"Server"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Footer with navigation links
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="https://github.com/herocode/supervisor" target="_blank" class="nav-link">
|
||||||
|
<span class="nav-icon">{"📖"}</span>
|
||||||
|
<span class="nav-text">{"Documentation"}</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/herocode/supervisor/issues" target="_blank" class="nav-link">
|
||||||
|
<span class="nav-icon">{"🐛"}</span>
|
||||||
|
<span class="nav-text">{"Report Issue"}</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="nav-link">
|
||||||
|
<span class="nav-icon">{"⚙️"}</span>
|
||||||
|
<span class="nav-text">{"Settings"}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="version-info">
|
||||||
|
{"Hero Supervisor v0.1.0"}
|
||||||
|
}
|
||||||
|
|
||||||
|
is_loading.set(true);
|
||||||
|
let is_loading = is_loading.clone();
|
||||||
|
let on_session_secret_change = on_session_secret_change.clone();
|
||||||
|
let on_supervisor_info_loaded = on_supervisor_info_loaded.clone();
|
||||||
|
let server_url = server_url.clone();
|
||||||
|
let session_secret_input = session_secret_input.clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
let client = WasmSupervisorClient::new(server_url.clone());
|
||||||
|
|
||||||
|
match client.discover().await {
|
||||||
|
Ok(_) => {
|
||||||
|
console::log!("Connected to supervisor successfully");
|
||||||
|
|
||||||
|
let secret_type = if secret.starts_with("admin_") {
|
||||||
|
SessionSecretType::Admin
|
||||||
|
} else if secret.starts_with("user_") {
|
||||||
|
SessionSecretType::User
|
||||||
|
} else {
|
||||||
|
SessionSecretType::User
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save to localStorage
|
||||||
|
let session_data = SessionData {
|
||||||
|
secret: secret.clone(),
|
||||||
|
secret_type: secret_type.clone(),
|
||||||
|
};
|
||||||
|
let _ = LocalStorage::set(SESSION_STORAGE_KEY, &session_data);
|
||||||
|
|
||||||
|
let supervisor_info = SupervisorInfo {
|
||||||
|
server_url: server_url.clone(),
|
||||||
|
admin_secrets_count: if secret_type == SessionSecretType::Admin { 1 } else { 0 },
|
||||||
|
user_secrets_count: 1,
|
||||||
|
register_secrets_count: 0,
|
||||||
|
runners_count: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
on_session_secret_change.emit((secret.clone(), secret_type.clone()));
|
||||||
|
on_supervisor_info_loaded.emit(supervisor_info);
|
||||||
|
session_secret_input.set(String::new());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
console::error!("Failed to connect to supervisor:", format!("{:?}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is_loading.set(false);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_logout = {
|
||||||
|
let on_session_secret_change = props.on_session_secret_change.clone();
|
||||||
|
Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
|
// Clear localStorage
|
||||||
|
let _ = LocalStorage::delete(SESSION_STORAGE_KEY);
|
||||||
|
on_session_secret_change.emit((String::new(), SessionSecretType::None));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="sidebar">
|
||||||
|
// Header with logo and title
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="logo-section">
|
||||||
|
<div class="logo">{"⚡"}</div>
|
||||||
|
<h1 class="title">{"Supervisor"}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Main control island
|
||||||
|
<div class="control-island">
|
||||||
|
// Session Login Section
|
||||||
|
<div class="session-section">
|
||||||
|
<h3 class="section-title">{"Session Login"}</h3>
|
||||||
|
|
||||||
|
if props.session_secret.is_empty() {
|
||||||
|
<div class="login-form">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="secret-input"
|
||||||
|
placeholder="Enter session secret"
|
||||||
|
value={(*session_secret_input).clone()}
|
||||||
|
onchange={on_session_secret_change}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="connect-btn"
|
||||||
|
onclick={on_session_secret_submit}
|
||||||
|
disabled={*is_loading}
|
||||||
|
>
|
||||||
|
if *is_loading {
|
||||||
|
<span class="loading-spinner"></span>
|
||||||
|
{"Connecting"}
|
||||||
|
} else {
|
||||||
|
{"🔐 Connect"}
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="login-hint">
|
||||||
|
{"Use admin_ or user_ prefixed secrets"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="session-active">
|
||||||
|
<div class="session-status">
|
||||||
|
<div class="status-indicator"></div>
|
||||||
|
<div class="status-info">
|
||||||
|
<span class="status-text">{"Connected"}</span>
|
||||||
|
<span class="session-badge">{
|
||||||
|
match props.session_secret_type {
|
||||||
|
SessionSecretType::Admin => "Admin",
|
||||||
|
SessionSecretType::User => "User",
|
||||||
|
SessionSecretType::None => "None",
|
||||||
|
}
|
||||||
|
}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="logout-btn"
|
||||||
|
onclick={on_logout}
|
||||||
|
>
|
||||||
|
{"🚪 Logout"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Secret Management Section (Admin only)
|
||||||
|
if props.session_secret_type == SessionSecretType::Admin && !props.session_secret.is_empty() {
|
||||||
|
<div class="secrets-section">
|
||||||
|
<h3 class="section-title">{"Secret Management"}</h3>
|
||||||
|
<div class="secret-display">
|
||||||
|
<div class="secret-item">
|
||||||
|
<label class="secret-label">{"Current Session Secret"}</label>
|
||||||
|
<div class="secret-value">
|
||||||
|
<code>{&props.session_secret}</code>
|
||||||
|
<button class="copy-btn" title="Copy to clipboard">{"📋"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server Info Section
|
||||||
|
if let Some(info) = &props.supervisor_info {
|
||||||
|
<div class="server-info-section">
|
||||||
|
<h3 class="section-title">{"Server Status"}</h3>
|
||||||
|
<div class="info-cards">
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-icon">{"🏃"}</div>
|
||||||
|
<div class="info-content">
|
||||||
|
<div class="info-number">{info.runners_count}</div>
|
||||||
|
<div class="info-label">{"Runners"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="info-icon">{"🔗"}</div>
|
||||||
|
<div class="info-content">
|
||||||
|
<div class="info-text">{&info.server_url.replace("http://", "").replace("https://", "")}</div>
|
||||||
|
<div class="info-label">{"Server"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Footer with navigation links
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="https://github.com/herocode/supervisor" target="_blank" class="nav-link">
|
||||||
|
<span class="nav-icon">{"📖"}</span>
|
||||||
|
<span class="nav-text">{"Documentation"}</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/herocode/supervisor/issues" target="_blank" class="nav-link">
|
||||||
|
<span class="nav-icon">{"🐛"}</span>
|
||||||
|
<span class="nav-text">{"Report Issue"}</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="nav-link">
|
||||||
|
<span class="nav-icon">{"⚙️"}</span>
|
||||||
|
<span class="nav-text">{"Settings"}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="version-info">
|
||||||
|
{"Hero Supervisor v0.1.0"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
let on_session_secret_change = {
|
||||||
|
let session_secret_input = session_secret_input.clone();
|
||||||
|
Callback::from(move |e: web_sys::Event| {
|
||||||
|
let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
|
||||||
|
session_secret_input.set(input.value());
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_session_secret_submit = {
|
||||||
|
let session_secret_input = session_secret_input.clone();
|
||||||
|
let is_loading = is_loading.clone();
|
||||||
|
let on_session_secret_change = props.on_session_secret_change.clone();
|
||||||
|
let on_supervisor_info_loaded = props.on_supervisor_info_loaded.clone();
|
||||||
|
let server_url = props.server_url.clone();
|
||||||
|
|
||||||
|
Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
|
let secret = (*session_secret_input).clone();
|
||||||
|
if secret.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_loading.set(true);
|
||||||
|
let is_loading = is_loading.clone();
|
||||||
|
let on_session_secret_change = on_session_secret_change.clone();
|
||||||
|
let on_supervisor_info_loaded = on_supervisor_info_loaded.clone();
|
||||||
|
let server_url = server_url.clone();
|
||||||
|
let session_secret_input = session_secret_input.clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
let client = WasmSupervisorClient::new(server_url.clone());
|
||||||
|
|
||||||
|
match client.discover().await {
|
||||||
|
Ok(_) => {
|
||||||
|
console::log!("Connected to supervisor successfully");
|
||||||
|
|
||||||
|
let secret_type = if secret.starts_with("admin_") {
|
||||||
|
SessionSecretType::Admin
|
||||||
|
} else if secret.starts_with("user_") {
|
||||||
|
SessionSecretType::User
|
||||||
|
} else {
|
||||||
|
SessionSecretType::User
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save to localStorage
|
||||||
|
let session_data = SessionData {
|
||||||
|
secret: secret.clone(),
|
||||||
|
secret_type: secret_type.clone(),
|
||||||
|
};
|
||||||
|
let _ = LocalStorage::set(SESSION_STORAGE_KEY, &session_data);
|
||||||
|
|
||||||
|
let supervisor_info = SupervisorInfo {
|
||||||
|
server_url: server_url.clone(),
|
||||||
|
admin_secrets_count: if secret_type == SessionSecretType::Admin { 1 } else { 0 },
|
||||||
|
user_secrets_count: 1,
|
||||||
|
register_secrets_count: 0,
|
||||||
|
runners_count: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
on_session_secret_change.emit((secret.clone(), secret_type.clone()));
|
||||||
|
on_supervisor_info_loaded.emit(supervisor_info);
|
||||||
|
session_secret_input.set(String::new());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
console::error!("Failed to connect to supervisor:", format!("{:?}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is_loading.set(false);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_logout = {
|
||||||
|
let on_session_secret_change = props.on_session_secret_change.clone();
|
||||||
|
Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
|
// Clear localStorage
|
||||||
|
let _ = LocalStorage::delete(SESSION_STORAGE_KEY);
|
||||||
|
on_session_secret_change.emit((String::new(), SessionSecretType::None));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="logo-section">
|
||||||
|
<div class="logo">{"⚡"}</div>
|
||||||
|
<h1 class="title">{"Supervisor"}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-island">
|
||||||
|
<div class="session-section">
|
||||||
|
<h3 class="section-title">{"Session Login"}</h3>
|
||||||
|
|
||||||
|
if props.session_secret.is_empty() {
|
||||||
|
<div class="login-form">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="secret-input"
|
||||||
|
placeholder="Enter session secret"
|
||||||
|
value={(*session_secret_input).clone()}
|
||||||
|
onchange={on_session_secret_change}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="connect-btn"
|
||||||
|
onclick={on_session_secret_submit}
|
||||||
|
disabled={*is_loading}
|
||||||
|
>
|
||||||
|
if *is_loading {
|
||||||
|
<span class="loading-spinner"></span>
|
||||||
|
{"Connecting"}
|
||||||
|
} else {
|
||||||
|
{"🔐 Connect"}
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="login-hint">
|
||||||
|
{"Use admin_ or user_ prefixed secrets"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="session-active">
|
||||||
|
<div class="session-status">
|
||||||
|
<div class="status-indicator"></div>
|
||||||
|
<div class="status-info">
|
||||||
|
<span class="status-text">{"Connected"}</span>
|
||||||
|
<span class="session-badge">{
|
||||||
|
match props.session_secret_type {
|
||||||
|
SessionSecretType::Admin => "Admin",
|
||||||
|
SessionSecretType::User => "User",
|
||||||
|
SessionSecretType::None => "None",
|
||||||
|
}
|
||||||
|
}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="logout-btn"
|
||||||
|
onclick={on_logout}
|
||||||
|
>
|
||||||
|
{"🚪 Logout"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Supervisor Info Section
|
||||||
|
if let Some(info) = &props.supervisor_info {
|
||||||
|
<div class="supervisor-info">
|
||||||
|
<div class="supervisor-info-header">
|
||||||
|
<span class="supervisor-info-title">{"Supervisor Info"}</span>
|
||||||
|
</div>
|
||||||
|
<div class="supervisor-info-content">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">{"Admin secrets:"}</span>
|
||||||
|
<span class="info-value">{info.admin_secrets_count}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">{"User secrets:"}</span>
|
||||||
|
<span class="info-value">{info.user_secrets_count}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">{"Register secrets:"}</span>
|
||||||
|
<span class="info-value">{info.register_secrets_count}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">{"Runners:"}</span>
|
||||||
|
<span class="info-value">{info.runners_count}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ ... }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
165
clients/admin-ui/src/toast.rs
Normal file
165
clients/admin-ui/src/toast.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
//! Toast notification component for displaying errors, warnings, and info messages
|
||||||
|
|
||||||
|
use yew::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use gloo::timers::callback::Timeout;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum ToastType {
|
||||||
|
Error,
|
||||||
|
Warning,
|
||||||
|
Info,
|
||||||
|
Success,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToastType {
|
||||||
|
pub fn css_class(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ToastType::Error => "toast-error",
|
||||||
|
ToastType::Warning => "toast-warning",
|
||||||
|
ToastType::Info => "toast-info",
|
||||||
|
ToastType::Success => "toast-success",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ToastType::Error => "❌",
|
||||||
|
ToastType::Warning => "⚠️",
|
||||||
|
ToastType::Info => "ℹ️",
|
||||||
|
ToastType::Success => "✅",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bg_class(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ToastType::Error => "bg-danger",
|
||||||
|
ToastType::Warning => "bg-warning",
|
||||||
|
ToastType::Info => "bg-info",
|
||||||
|
ToastType::Success => "bg-success",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Toast {
|
||||||
|
pub id: String,
|
||||||
|
pub message: String,
|
||||||
|
pub toast_type: ToastType,
|
||||||
|
pub timestamp: f64,
|
||||||
|
pub auto_dismiss: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Toast {
|
||||||
|
pub fn new(message: String, toast_type: ToastType) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Uuid::new_v4().to_string(),
|
||||||
|
message,
|
||||||
|
toast_type,
|
||||||
|
timestamp: js_sys::Date::now(),
|
||||||
|
auto_dismiss: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(message: String) -> Self {
|
||||||
|
Self::new(message, ToastType::Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warning(message: String) -> Self {
|
||||||
|
Self::new(message, ToastType::Warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info(message: String) -> Self {
|
||||||
|
Self::new(message, ToastType::Info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn success(message: String) -> Self {
|
||||||
|
Self::new(message, ToastType::Success)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persistent(mut self) -> Self {
|
||||||
|
self.auto_dismiss = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct ToastContainerProps {
|
||||||
|
pub toasts: Vec<Toast>,
|
||||||
|
pub on_dismiss: Callback<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(ToastContainer)]
|
||||||
|
pub fn toast_container(props: &ToastContainerProps) -> Html {
|
||||||
|
let timeouts = use_mut_ref(HashMap::<String, Timeout>::new);
|
||||||
|
|
||||||
|
// Auto-dismiss toasts after 5 seconds
|
||||||
|
use_effect_with(props.toasts.clone(), {
|
||||||
|
let on_dismiss = props.on_dismiss.clone();
|
||||||
|
let timeouts = timeouts.clone();
|
||||||
|
move |toasts| {
|
||||||
|
for toast in toasts {
|
||||||
|
if toast.auto_dismiss {
|
||||||
|
let toast_id = toast.id.clone();
|
||||||
|
let on_dismiss = on_dismiss.clone();
|
||||||
|
let timeout = Timeout::new(5000, move || {
|
||||||
|
on_dismiss.emit(toast_id);
|
||||||
|
});
|
||||||
|
timeouts.borrow_mut().insert(toast.id.clone(), timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
move || {
|
||||||
|
timeouts.borrow_mut().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1055;">
|
||||||
|
{for props.toasts.iter().map(|toast| {
|
||||||
|
let on_dismiss = {
|
||||||
|
let on_dismiss = props.on_dismiss.clone();
|
||||||
|
let toast_id = toast.id.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
on_dismiss.emit(toast_id.clone());
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div key={toast.id.clone()} class={classes!("toast", "show", "mb-2")} role="alert">
|
||||||
|
<div class={classes!("toast-header", toast.toast_type.bg_class(), "text-white")}>
|
||||||
|
<span class="me-2">{toast.toast_type.icon()}</span>
|
||||||
|
<strong class="me-auto">{
|
||||||
|
match toast.toast_type {
|
||||||
|
ToastType::Error => "Error",
|
||||||
|
ToastType::Warning => "Warning",
|
||||||
|
ToastType::Info => "Info",
|
||||||
|
ToastType::Success => "Success",
|
||||||
|
}
|
||||||
|
}</strong>
|
||||||
|
<small class="text-white-50">{format_timestamp(toast.timestamp)}</small>
|
||||||
|
<button type="button" class="btn-close btn-close-white ms-2" onclick={on_dismiss}></button>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body bg-dark text-white">
|
||||||
|
{toast.message.clone()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_timestamp(timestamp: f64) -> String {
|
||||||
|
let now = js_sys::Date::now();
|
||||||
|
let diff = (now - timestamp) / 1000.0; // seconds
|
||||||
|
|
||||||
|
if diff < 60.0 {
|
||||||
|
"now".to_string()
|
||||||
|
} else if diff < 3600.0 {
|
||||||
|
format!("{}m ago", (diff / 60.0) as u32)
|
||||||
|
} else {
|
||||||
|
format!("{}h ago", (diff / 3600.0) as u32)
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
139
clients/openrpc/Cargo.lock
generated
139
clients/openrpc/Cargo.lock
generated
@@ -331,15 +331,6 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "deranged"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
|
||||||
dependencies = [
|
|
||||||
"powerfmt",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -426,7 +417,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-executor",
|
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
@@ -449,17 +439,6 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-executor"
|
|
||||||
version = "0.3.31"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -585,19 +564,34 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hero-job"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"log",
|
||||||
|
"redis",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hero-supervisor"
|
name = "hero-supervisor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger 0.10.2",
|
"env_logger 0.10.2",
|
||||||
|
"hero-job",
|
||||||
"jsonrpsee",
|
"jsonrpsee",
|
||||||
"log",
|
"log",
|
||||||
|
"rand",
|
||||||
"redis",
|
"redis",
|
||||||
"sal-service-manager",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -616,6 +610,7 @@ dependencies = [
|
|||||||
"console_log",
|
"console_log",
|
||||||
"env_logger 0.11.8",
|
"env_logger 0.11.8",
|
||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
|
"hero-job",
|
||||||
"hero-supervisor",
|
"hero-supervisor",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"jsonrpsee",
|
"jsonrpsee",
|
||||||
@@ -1155,12 +1150,6 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-conv"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -1258,19 +1247,6 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "plist"
|
|
||||||
version = "1.7.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1"
|
|
||||||
dependencies = [
|
|
||||||
"base64",
|
|
||||||
"indexmap",
|
|
||||||
"quick-xml",
|
|
||||||
"serde",
|
|
||||||
"time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -1295,12 +1271,6 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "powerfmt"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -1328,15 +1298,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quick-xml"
|
|
||||||
version = "0.38.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
@@ -1559,23 +1520,6 @@ version = "1.0.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sal-service-manager"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"chrono",
|
|
||||||
"futures",
|
|
||||||
"log",
|
|
||||||
"once_cell",
|
|
||||||
"plist",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"zinit-client",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -1813,37 +1757,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time"
|
|
||||||
version = "0.3.41"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
|
||||||
dependencies = [
|
|
||||||
"deranged",
|
|
||||||
"itoa",
|
|
||||||
"num-conv",
|
|
||||||
"powerfmt",
|
|
||||||
"serde",
|
|
||||||
"time-core",
|
|
||||||
"time-macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time-core"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time-macros"
|
|
||||||
version = "0.2.22"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
|
||||||
dependencies = [
|
|
||||||
"num-conv",
|
|
||||||
"time-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2690,21 +2603,3 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zinit-client"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4121c3ba22f1b3ccc4546de32072c9530c7e2735b734641ada5280ac422ac9cd"
|
|
||||||
dependencies = [
|
|
||||||
"async-stream",
|
|
||||||
"async-trait",
|
|
||||||
"chrono",
|
|
||||||
"futures",
|
|
||||||
"rand",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
@@ -10,6 +10,8 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Common dependencies for both native and WASM
|
# Common dependencies for both native and WASM
|
||||||
|
hero-supervisor = { path = "../../" }
|
||||||
|
hero-job = { path = "../../../job" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
@@ -27,14 +27,10 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
// Import types from the main supervisor crate
|
// Import types from the main supervisor crate
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use hero_supervisor::RunnerStatus;
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
@@ -132,13 +128,12 @@ pub enum RunnerType {
|
|||||||
PyRunner,
|
PyRunner,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process manager type for a runner
|
/// Process manager type for WASM compatibility
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum ProcessManagerType {
|
pub enum ProcessManagerType {
|
||||||
/// Simple process manager for direct process spawning
|
|
||||||
Simple,
|
Simple,
|
||||||
/// Tmux process manager for session-based management
|
Tmux(String),
|
||||||
Tmux(String), // session name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for an actor runner
|
/// Configuration for an actor runner
|
||||||
@@ -157,16 +152,6 @@ pub struct RunnerConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Job status enumeration
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum JobStatus {
|
|
||||||
Dispatched,
|
|
||||||
WaitingForPrerequisites,
|
|
||||||
Started,
|
|
||||||
Error,
|
|
||||||
Stopping,
|
|
||||||
Finished,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Job result response
|
/// Job result response
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -186,30 +171,8 @@ pub struct JobStatusResponse {
|
|||||||
pub completed_at: Option<String>,
|
pub completed_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Job structure for creating and managing jobs
|
// Re-export Job types from shared crate
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
pub use hero_job::{Job, JobStatus, JobError, JobBuilder};
|
||||||
pub struct Job {
|
|
||||||
/// Unique job identifier
|
|
||||||
pub id: String,
|
|
||||||
/// ID of the caller/client that created this job
|
|
||||||
pub caller_id: String,
|
|
||||||
/// Context ID for grouping related jobs
|
|
||||||
pub context_id: String,
|
|
||||||
/// Script content or payload to execute
|
|
||||||
pub payload: String,
|
|
||||||
/// Name of the specific runner/actor to execute this job
|
|
||||||
pub runner: String,
|
|
||||||
/// Name of the executor the runner will use to execute this job
|
|
||||||
pub executor: String,
|
|
||||||
/// Job execution timeout (in seconds)
|
|
||||||
pub timeout: u64,
|
|
||||||
/// Environment variables for job execution
|
|
||||||
pub env_vars: HashMap<String, String>,
|
|
||||||
/// Timestamp when the job was created
|
|
||||||
pub created_at: String,
|
|
||||||
/// Timestamp when the job was last updated
|
|
||||||
pub updated_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process status wrapper for OpenRPC serialization (matches server response)
|
/// Process status wrapper for OpenRPC serialization (matches server response)
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
@@ -245,9 +208,21 @@ pub type ProcessStatus = ProcessStatusWrapper;
|
|||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub type LogInfo = LogInfoWrapper;
|
pub type LogInfo = LogInfoWrapper;
|
||||||
|
|
||||||
|
/// Simple ProcessStatus type for native builds to avoid service manager dependency
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub type ProcessStatus = ProcessStatusWrapper;
|
||||||
|
|
||||||
/// Re-export types from supervisor crate for native builds
|
/// Re-export types from supervisor crate for native builds
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use hero_supervisor::{LogInfo, RunnerStatus as ProcessStatus};
|
pub use hero_supervisor::{ProcessManagerType, RunnerStatus};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub use hero_supervisor::runner::LogInfo;
|
||||||
|
|
||||||
|
/// Type aliases for WASM compatibility
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub type RunnerStatus = ProcessStatusWrapper;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub type LogInfo = LogInfoWrapper;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl SupervisorClient {
|
impl SupervisorClient {
|
||||||
@@ -425,31 +400,12 @@ impl SupervisorClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get status of a specific runner
|
/// Get status of a specific runner
|
||||||
pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult<ProcessStatus> {
|
pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult<RunnerStatus> {
|
||||||
#[cfg(target_arch = "wasm32")]
|
let status: RunnerStatus = self
|
||||||
{
|
.client
|
||||||
let status: ProcessStatusWrapper = self
|
.request("get_runner_status", rpc_params![actor_id])
|
||||||
.client
|
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||||
.request("get_runner_status", rpc_params![actor_id])
|
Ok(status)
|
||||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
|
||||||
Ok(status)
|
|
||||||
}
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
{
|
|
||||||
let status: ProcessStatusWrapper = self
|
|
||||||
.client
|
|
||||||
.request("get_runner_status", rpc_params![actor_id])
|
|
||||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
|
||||||
// Convert wrapper to internal type for native builds
|
|
||||||
let internal_status = match status {
|
|
||||||
ProcessStatusWrapper::Running => RunnerStatus::Running,
|
|
||||||
ProcessStatusWrapper::Stopped => RunnerStatus::Stopped,
|
|
||||||
ProcessStatusWrapper::Starting => RunnerStatus::Starting,
|
|
||||||
ProcessStatusWrapper::Stopping => RunnerStatus::Stopping,
|
|
||||||
ProcessStatusWrapper::Error(msg) => RunnerStatus::Error(msg),
|
|
||||||
};
|
|
||||||
Ok(internal_status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get logs for a specific runner
|
/// Get logs for a specific runner
|
||||||
@@ -459,28 +415,11 @@ impl SupervisorClient {
|
|||||||
lines: Option<usize>,
|
lines: Option<usize>,
|
||||||
follow: bool,
|
follow: bool,
|
||||||
) -> ClientResult<Vec<LogInfo>> {
|
) -> ClientResult<Vec<LogInfo>> {
|
||||||
#[cfg(target_arch = "wasm32")]
|
let logs: Vec<LogInfo> = self
|
||||||
{
|
.client
|
||||||
let logs: Vec<LogInfoWrapper> = self
|
.request("get_runner_logs", rpc_params![actor_id, lines, follow])
|
||||||
.client
|
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||||
.request("get_runner_logs", rpc_params![actor_id, lines, follow])
|
Ok(logs)
|
||||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
|
||||||
Ok(logs)
|
|
||||||
}
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
{
|
|
||||||
let logs: Vec<LogInfoWrapper> = self
|
|
||||||
.client
|
|
||||||
.request("get_runner_logs", rpc_params![actor_id, lines, follow])
|
|
||||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
|
||||||
// Convert wrapper to internal type for native builds
|
|
||||||
let internal_logs = logs.into_iter().map(|log| hero_supervisor::LogInfo {
|
|
||||||
timestamp: log.timestamp,
|
|
||||||
level: log.level,
|
|
||||||
message: log.message,
|
|
||||||
}).collect();
|
|
||||||
Ok(internal_logs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue a job to a specific runner
|
/// Queue a job to a specific runner
|
||||||
@@ -497,8 +436,7 @@ impl SupervisorClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue a job to a specific runner and wait for the result
|
/// Queue a job and wait for completion
|
||||||
/// This implements the proper Hero job protocol with BLPOP on reply queue
|
|
||||||
pub async fn queue_and_wait(&self, runner: &str, job: Job, timeout_secs: u64) -> ClientResult<Option<String>> {
|
pub async fn queue_and_wait(&self, runner: &str, job: Job, timeout_secs: u64) -> ClientResult<Option<String>> {
|
||||||
let params = serde_json::json!({
|
let params = serde_json::json!({
|
||||||
"runner": runner,
|
"runner": runner,
|
||||||
@@ -512,6 +450,20 @@ impl SupervisorClient {
|
|||||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run a job on a specific runner
|
||||||
|
pub async fn run_job(&self, secret: &str, job: Job) -> ClientResult<JobResult> {
|
||||||
|
let params = serde_json::json!({
|
||||||
|
"secret": secret,
|
||||||
|
"job": job
|
||||||
|
});
|
||||||
|
|
||||||
|
let result: JobResult = self
|
||||||
|
.client
|
||||||
|
.request("run_job", rpc_params![params])
|
||||||
|
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get job result by job ID
|
/// Get job result by job ID
|
||||||
pub async fn get_job_result(&self, job_id: &str) -> ClientResult<Option<String>> {
|
pub async fn get_job_result(&self, job_id: &str) -> ClientResult<Option<String>> {
|
||||||
@@ -523,31 +475,12 @@ impl SupervisorClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get status of all runners
|
/// Get status of all runners
|
||||||
pub async fn get_all_runner_status(&self) -> ClientResult<Vec<(String, ProcessStatus)>> {
|
pub async fn get_all_runner_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
|
||||||
let statuses: Vec<(String, ProcessStatusWrapper)> = self
|
let statuses: Vec<(String, RunnerStatus)> = self
|
||||||
.client
|
.client
|
||||||
.request("get_all_runner_status", rpc_params![])
|
.request("get_all_runner_status", rpc_params![])
|
||||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||||
|
Ok(statuses)
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
{
|
|
||||||
Ok(statuses)
|
|
||||||
}
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
{
|
|
||||||
// Convert wrapper to internal type for native builds
|
|
||||||
let internal_statuses = statuses.into_iter().map(|(name, status)| {
|
|
||||||
let internal_status = match status {
|
|
||||||
ProcessStatusWrapper::Running => RunnerStatus::Running,
|
|
||||||
ProcessStatusWrapper::Stopped => RunnerStatus::Stopped,
|
|
||||||
ProcessStatusWrapper::Starting => RunnerStatus::Starting,
|
|
||||||
ProcessStatusWrapper::Stopping => RunnerStatus::Stopping,
|
|
||||||
ProcessStatusWrapper::Error(msg) => RunnerStatus::Error(msg),
|
|
||||||
};
|
|
||||||
(name, internal_status)
|
|
||||||
}).collect();
|
|
||||||
Ok(internal_statuses)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start all runners
|
/// Start all runners
|
||||||
@@ -569,31 +502,12 @@ impl SupervisorClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get status of all runners (alternative method)
|
/// Get status of all runners (alternative method)
|
||||||
pub async fn get_all_status(&self) -> ClientResult<Vec<(String, ProcessStatus)>> {
|
pub async fn get_all_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
|
||||||
let statuses: Vec<(String, ProcessStatusWrapper)> = self
|
let statuses: Vec<(String, RunnerStatus)> = self
|
||||||
.client
|
.client
|
||||||
.request("get_all_status", rpc_params![])
|
.request("get_all_status", rpc_params![])
|
||||||
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
.await.map_err(|e| ClientError::JsonRpc(e))?;
|
||||||
|
Ok(statuses)
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
{
|
|
||||||
Ok(statuses)
|
|
||||||
}
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
{
|
|
||||||
// Convert wrapper to internal type for native builds
|
|
||||||
let internal_statuses = statuses.into_iter().map(|(name, status)| {
|
|
||||||
let internal_status = match status {
|
|
||||||
ProcessStatusWrapper::Running => RunnerStatus::Running,
|
|
||||||
ProcessStatusWrapper::Stopped => RunnerStatus::Stopped,
|
|
||||||
ProcessStatusWrapper::Starting => RunnerStatus::Starting,
|
|
||||||
ProcessStatusWrapper::Stopping => RunnerStatus::Stopping,
|
|
||||||
ProcessStatusWrapper::Error(msg) => RunnerStatus::Error(msg),
|
|
||||||
};
|
|
||||||
(name, internal_status)
|
|
||||||
}).collect();
|
|
||||||
Ok(internal_statuses)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a secret to the supervisor
|
/// Add a secret to the supervisor
|
||||||
@@ -683,129 +597,6 @@ impl SupervisorClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for creating jobs with a fluent API
|
|
||||||
pub struct JobBuilder {
|
|
||||||
caller_id: String,
|
|
||||||
context_id: String,
|
|
||||||
payload: String,
|
|
||||||
runner: String,
|
|
||||||
executor: String,
|
|
||||||
timeout: u64, // timeout in seconds
|
|
||||||
env_vars: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JobBuilder {
|
|
||||||
/// Create a new job builder
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
caller_id: "".to_string(),
|
|
||||||
context_id: "".to_string(),
|
|
||||||
payload: "".to_string(),
|
|
||||||
runner: "".to_string(),
|
|
||||||
executor: "".to_string(),
|
|
||||||
timeout: 300, // 5 minutes default
|
|
||||||
env_vars: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the caller ID for this job
|
|
||||||
pub fn caller_id(mut self, caller_id: impl Into<String>) -> Self {
|
|
||||||
self.caller_id = caller_id.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the context ID for this job
|
|
||||||
pub fn context_id(mut self, context_id: impl Into<String>) -> Self {
|
|
||||||
self.context_id = context_id.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the payload (script content) for this job
|
|
||||||
pub fn payload(mut self, payload: impl Into<String>) -> Self {
|
|
||||||
self.payload = payload.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the executor for this job
|
|
||||||
pub fn executor(mut self, executor: impl Into<String>) -> Self {
|
|
||||||
self.executor = executor.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the runner name for this job
|
|
||||||
pub fn runner(mut self, runner: impl Into<String>) -> Self {
|
|
||||||
self.runner = runner.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the timeout for job execution (in seconds)
|
|
||||||
pub fn timeout(mut self, timeout: u64) -> Self {
|
|
||||||
self.timeout = timeout;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a single environment variable
|
|
||||||
pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
|
||||||
self.env_vars.insert(key.into(), value.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set multiple environment variables from a HashMap
|
|
||||||
pub fn env_vars(mut self, env_vars: HashMap<String, String>) -> Self {
|
|
||||||
self.env_vars = env_vars;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the job
|
|
||||||
pub fn build(self) -> ClientResult<Job> {
|
|
||||||
if self.caller_id.is_empty() {
|
|
||||||
return Err(ClientError::Server {
|
|
||||||
message: "caller_id is required".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if self.context_id.is_empty() {
|
|
||||||
return Err(ClientError::Server {
|
|
||||||
message: "context_id is required".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if self.payload.is_empty() {
|
|
||||||
return Err(ClientError::Server {
|
|
||||||
message: "payload is required".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if self.runner.is_empty() {
|
|
||||||
return Err(ClientError::Server {
|
|
||||||
message: "runner is required".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if self.executor.is_empty() {
|
|
||||||
return Err(ClientError::Server {
|
|
||||||
message: "executor is required".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let now = chrono::Utc::now().to_rfc3339();
|
|
||||||
|
|
||||||
Ok(Job {
|
|
||||||
id: Uuid::new_v4().to_string(),
|
|
||||||
caller_id: self.caller_id,
|
|
||||||
context_id: self.context_id,
|
|
||||||
payload: self.payload,
|
|
||||||
runner: self.runner,
|
|
||||||
executor: self.executor,
|
|
||||||
timeout: self.timeout,
|
|
||||||
env_vars: self.env_vars,
|
|
||||||
created_at: now.clone(),
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for JobBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
@@ -206,7 +206,7 @@ impl WasmSupervisorClient {
|
|||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
match self.call_method("run_job", params).await {
|
match self.call_method("jobs.run", params).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
if let Some(result_str) = result.as_str() {
|
if let Some(result_str) = result.as_str() {
|
||||||
Ok(result_str.to_string())
|
Ok(result_str.to_string())
|
||||||
@@ -234,7 +234,7 @@ impl WasmSupervisorClient {
|
|||||||
|
|
||||||
/// List all job IDs from Redis
|
/// List all job IDs from Redis
|
||||||
pub async fn list_jobs(&self) -> Result<Vec<String>, JsValue> {
|
pub async fn list_jobs(&self) -> Result<Vec<String>, JsValue> {
|
||||||
match self.call_method("list_jobs", serde_json::Value::Null).await {
|
match self.call_method("jobs.list", serde_json::Value::Null).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
if let Ok(jobs) = serde_json::from_value::<Vec<String>>(result) {
|
if let Ok(jobs) = serde_json::from_value::<Vec<String>>(result) {
|
||||||
Ok(jobs)
|
Ok(jobs)
|
||||||
|
35
src/app.rs
35
src/app.rs
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
use crate::Supervisor;
|
use crate::Supervisor;
|
||||||
use crate::openrpc::start_openrpc_servers;
|
use crate::openrpc::start_openrpc_servers;
|
||||||
|
use crate::mycelium::MyceliumServer;
|
||||||
use log::{info, error, debug};
|
use log::{info, error, debug};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@@ -42,6 +43,9 @@ impl SupervisorApp {
|
|||||||
// Start OpenRPC server
|
// Start OpenRPC server
|
||||||
self.start_openrpc_server().await?;
|
self.start_openrpc_server().await?;
|
||||||
|
|
||||||
|
// Start Mycelium server
|
||||||
|
self.start_mycelium_server().await?;
|
||||||
|
|
||||||
// Set up graceful shutdown
|
// Set up graceful shutdown
|
||||||
self.setup_graceful_shutdown().await;
|
self.setup_graceful_shutdown().await;
|
||||||
|
|
||||||
@@ -146,6 +150,37 @@ impl SupervisorApp {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start the Mycelium server
|
||||||
|
async fn start_mycelium_server(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
info!("Starting Mycelium server...");
|
||||||
|
|
||||||
|
let supervisor_for_mycelium = Arc::new(Mutex::new(self.supervisor.clone()));
|
||||||
|
let mycelium_port = 8990; // Standard Mycelium port
|
||||||
|
let bind_address = "127.0.0.1".to_string();
|
||||||
|
|
||||||
|
let mycelium_server = MyceliumServer::new(
|
||||||
|
supervisor_for_mycelium,
|
||||||
|
bind_address,
|
||||||
|
mycelium_port,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start the Mycelium server in a background task
|
||||||
|
let server_handle = tokio::spawn(async move {
|
||||||
|
if let Err(e) = mycelium_server.start().await {
|
||||||
|
error!("Mycelium server error: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Give the server a moment to start
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
info!("Mycelium server started successfully on port {}", mycelium_port);
|
||||||
|
|
||||||
|
// Store the handle for potential cleanup
|
||||||
|
std::mem::forget(server_handle); // For now, let it run in background
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Get status of all runners
|
/// Get status of all runners
|
||||||
pub async fn get_status(&self) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> {
|
pub async fn get_status(&self) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> {
|
||||||
debug!("Getting status of all runners");
|
debug!("Getting status of all runners");
|
||||||
|
328
src/client.rs
328
src/client.rs
@@ -1,328 +0,0 @@
|
|||||||
//! Main supervisor implementation for managing multiple actor runners.
|
|
||||||
|
|
||||||
use chrono::Utc;
|
|
||||||
use redis::AsyncCommands;
|
|
||||||
|
|
||||||
use crate::{runner::{RunnerError, RunnerResult}, job::JobStatus, JobError};
|
|
||||||
use crate::{job::Job};
|
|
||||||
|
|
||||||
/// Client for managing jobs in Redis
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Client {
|
|
||||||
redis_client: redis::Client,
|
|
||||||
namespace: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ClientBuilder {
|
|
||||||
/// Redis URL for connection
|
|
||||||
redis_url: String,
|
|
||||||
/// Namespace for queue keys
|
|
||||||
namespace: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientBuilder {
|
|
||||||
/// Create a new supervisor builder
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
redis_url: "redis://localhost:6379".to_string(),
|
|
||||||
namespace: "".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the Redis URL
|
|
||||||
pub fn redis_url<S: Into<String>>(mut self, url: S) -> Self {
|
|
||||||
self.redis_url = url.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the namespace for queue keys
|
|
||||||
pub fn namespace<S: Into<String>>(mut self, namespace: S) -> Self {
|
|
||||||
self.namespace = namespace.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the supervisor
|
|
||||||
pub async fn build(self) -> RunnerResult<Client> {
|
|
||||||
// Create Redis client
|
|
||||||
let redis_client = redis::Client::open(self.redis_url.as_str())
|
|
||||||
.map_err(|e| RunnerError::ConfigError {
|
|
||||||
reason: format!("Invalid Redis URL: {}", e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Client {
|
|
||||||
redis_client,
|
|
||||||
namespace: self.namespace,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Client {
|
|
||||||
fn default() -> Self {
|
|
||||||
// Note: Default implementation creates an empty supervisor
|
|
||||||
// Use Supervisor::builder() for proper initialization
|
|
||||||
Self {
|
|
||||||
redis_client: redis::Client::open("redis://localhost:6379").unwrap(),
|
|
||||||
namespace: "".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
/// Create a new supervisor builder
|
|
||||||
pub fn builder() -> ClientBuilder {
|
|
||||||
ClientBuilder::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List all job IDs from Redis
|
|
||||||
pub async fn list_jobs(&self) -> RunnerResult<Vec<String>> {
|
|
||||||
let mut conn = self.redis_client
|
|
||||||
.get_multiplexed_async_connection()
|
|
||||||
.await
|
|
||||||
.map_err(|e| RunnerError::RedisError { source: e })?;
|
|
||||||
|
|
||||||
let keys: Vec<String> = conn.keys(format!("{}:*", &self.jobs_key())).await?;
|
|
||||||
let job_ids: Vec<String> = keys
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|key| {
|
|
||||||
if key.starts_with(&format!("{}:", self.jobs_key())) {
|
|
||||||
key.strip_prefix(&format!("{}:", self.jobs_key()))
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(job_ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn jobs_key(&self) -> String {
|
|
||||||
if self.namespace.is_empty() {
|
|
||||||
format!("job")
|
|
||||||
} else {
|
|
||||||
format!("{}:job", self.namespace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn job_key(&self, job_id: &str) -> String {
|
|
||||||
if self.namespace.is_empty() {
|
|
||||||
format!("job:{}", job_id)
|
|
||||||
} else {
|
|
||||||
format!("{}:job:{}", self.namespace, job_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn job_reply_key(&self, job_id: &str) -> String {
|
|
||||||
if self.namespace.is_empty() {
|
|
||||||
format!("reply:{}", job_id)
|
|
||||||
} else {
|
|
||||||
format!("{}:reply:{}", self.namespace, job_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set job error in Redis
|
|
||||||
pub async fn set_error(&self,
|
|
||||||
job_id: &str,
|
|
||||||
error: &str,
|
|
||||||
) -> Result<(), JobError> {
|
|
||||||
let job_key = self.job_key(job_id);
|
|
||||||
let now = Utc::now();
|
|
||||||
|
|
||||||
let mut conn = self.redis_client
|
|
||||||
.get_multiplexed_async_connection()
|
|
||||||
.await
|
|
||||||
.map_err(|e| JobError:: Redis(e))?;
|
|
||||||
|
|
||||||
conn.hset_multiple(&job_key, &[
|
|
||||||
("error", error),
|
|
||||||
("status", JobStatus::Error.as_str()),
|
|
||||||
("updated_at", &now.to_rfc3339()),
|
|
||||||
]).await
|
|
||||||
.map_err(|e| JobError::Redis(e))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set job status in Redis
|
|
||||||
pub async fn set_job_status(&self,
|
|
||||||
job_id: &str,
|
|
||||||
status: JobStatus,
|
|
||||||
) -> Result<(), JobError> {
|
|
||||||
let job_key = self.job_key(job_id);
|
|
||||||
let now = Utc::now();
|
|
||||||
|
|
||||||
let mut conn = self.redis_client
|
|
||||||
.get_multiplexed_async_connection()
|
|
||||||
.await
|
|
||||||
.map_err(|e| JobError:: Redis(e))?;
|
|
||||||
|
|
||||||
conn.hset_multiple(&job_key, &[
|
|
||||||
("status", status.as_str()),
|
|
||||||
("updated_at", &now.to_rfc3339()),
|
|
||||||
]).await
|
|
||||||
.map_err(|e| JobError::Redis(e))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get job status from Redis
|
|
||||||
pub async fn get_status(
|
|
||||||
&self,
|
|
||||||
job_id: &str,
|
|
||||||
) -> Result<JobStatus, JobError> {
|
|
||||||
let mut conn = self.redis_client
|
|
||||||
.get_multiplexed_async_connection()
|
|
||||||
.await
|
|
||||||
.map_err(|e| JobError:: Redis(e))?;
|
|
||||||
|
|
||||||
let status_str: Option<String> = conn.hget(&self.job_key(job_id), "status").await?;
|
|
||||||
|
|
||||||
match status_str {
|
|
||||||
Some(s) => JobStatus::from_str(&s).ok_or_else(|| JobError::InvalidStatus(s)),
|
|
||||||
None => Err(JobError::NotFound(job_id.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete job from Redis
|
|
||||||
pub async fn delete_from_redis(
|
|
||||||
&self,
|
|
||||||
job_id: &str,
|
|
||||||
) -> Result<(), JobError> {
|
|
||||||
let mut conn = self.redis_client
|
|
||||||
.get_multiplexed_async_connection()
|
|
||||||
.await
|
|
||||||
.map_err(|e| JobError:: Redis(e))?;
|
|
||||||
|
|
||||||
let job_key = self.job_key(job_id);
|
|
||||||
let _: () = conn.del(&job_key).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store this job in Redis
|
|
||||||
pub async fn store_job_in_redis(&self, job: &Job) -> Result<(), JobError> {
|
|
||||||
let mut conn = self.redis_client
|
|
||||||
.get_multiplexed_async_connection()
|
|
||||||
.await
|
|
||||||
.map_err(|e| JobError:: Redis(e))?;
|
|
||||||
|
|
||||||
let job_key = self.job_key(&job.id);
|
|
||||||
|
|
||||||
// Serialize the job data
|
|
||||||
let job_data = serde_json::to_string(job)?;
|
|
||||||
|
|
||||||
// Store job data in Redis hash
|
|
||||||
let _: () = conn.hset_multiple(&job_key, &[
|
|
||||||
("data", job_data),
|
|
||||||
("status", JobStatus::Dispatched.as_str().to_string()),
|
|
||||||
("created_at", job.created_at.to_rfc3339()),
|
|
||||||
("updated_at", job.updated_at.to_rfc3339()),
|
|
||||||
]).await?;
|
|
||||||
|
|
||||||
// Set TTL for the job (24 hours)
|
|
||||||
let _: () = conn.expire(&job_key, 86400).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load a job from Redis by ID
|
|
||||||
pub async fn load_job_from_redis(
|
|
||||||
&self,
|
|
||||||
job_id: &str,
|
|
||||||
) -> Result<Job, JobError> {
|
|
||||||
let job_key = self.job_key(job_id);
|
|
||||||
|
|
||||||
let mut conn = self.redis_client
|
|
||||||
.get_multiplexed_async_connection()
|
|
||||||
.await
|
|
||||||
.map_err(|e| JobError:: Redis(e))?;
|
|
||||||
|
|
||||||
// Get job data from Redis
|
|
||||||
let job_data: Option<String> = conn.hget(&job_key, "data").await?;
|
|
||||||
|
|
||||||
match job_data {
|
|
||||||
Some(data) => {
|
|
||||||
let job: Job = serde_json::from_str(&data)?;
|
|
||||||
Ok(job)
|
|
||||||
}
|
|
||||||
None => Err(JobError::NotFound(job_id.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a job by ID
|
|
||||||
pub async fn delete_job(&mut self, job_id: &str) -> RunnerResult<()> {
|
|
||||||
use redis::AsyncCommands;
|
|
||||||
|
|
||||||
let mut conn = self.redis_client.get_multiplexed_async_connection().await
|
|
||||||
.map_err(|e| JobError:: Redis(e))?;
|
|
||||||
|
|
||||||
let job_key = self.job_key(job_id);
|
|
||||||
let deleted_count: i32 = conn.del(&job_key).await
|
|
||||||
.map_err(|e| RunnerError::QueueError {
|
|
||||||
actor_id: job_id.to_string(),
|
|
||||||
reason: format!("Failed to delete job: {}", e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if deleted_count == 0 {
|
|
||||||
return Err(RunnerError::QueueError {
|
|
||||||
actor_id: job_id.to_string(),
|
|
||||||
reason: format!("Job '{}' not found or already deleted", job_id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set job result in Redis
|
|
||||||
pub async fn set_result(
|
|
||||||
&self,
|
|
||||||
job_id: &str,
|
|
||||||
result: &str,
|
|
||||||
) -> Result<(), JobError> {
|
|
||||||
let job_key = self.job_key(&job_id);
|
|
||||||
let now = Utc::now();
|
|
||||||
let mut conn = self.redis_client
|
|
||||||
.get_multiplexed_async_connection()
|
|
||||||
.await
|
|
||||||
.map_err(|e| JobError:: Redis(e))?;
|
|
||||||
let _: () = conn.hset_multiple(&job_key, &[
|
|
||||||
("result", result),
|
|
||||||
("status", JobStatus::Finished.as_str()),
|
|
||||||
("updated_at", &now.to_rfc3339()),
|
|
||||||
]).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get job result from Redis
|
|
||||||
pub async fn get_result(
|
|
||||||
&self,
|
|
||||||
job_id: &str,
|
|
||||||
) -> Result<Option<String>, JobError> {
|
|
||||||
let job_key = self.job_key(job_id);
|
|
||||||
let mut conn = self.redis_client
|
|
||||||
.get_multiplexed_async_connection()
|
|
||||||
.await
|
|
||||||
.map_err(|e| JobError:: Redis(e))?;
|
|
||||||
let result: Option<String> = conn.hget(&job_key, "result").await?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a job ID from the work queue (blocking pop)
|
|
||||||
pub async fn get_job_id(&self, queue_key: &str) -> Result<Option<String>, JobError> {
|
|
||||||
let mut conn = self.redis_client
|
|
||||||
.get_multiplexed_async_connection()
|
|
||||||
.await
|
|
||||||
.map_err(|e| JobError::Redis(e))?;
|
|
||||||
|
|
||||||
// Use BRPOP with a short timeout to avoid blocking indefinitely
|
|
||||||
let result: Option<(String, String)> = conn.brpop(queue_key, 1.0).await
|
|
||||||
.map_err(|e| JobError::Redis(e))?;
|
|
||||||
|
|
||||||
Ok(result.map(|(_, job_id)| job_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a job by ID (alias for load_job_from_redis)
|
|
||||||
pub async fn get_job(&self, job_id: &str) -> Result<Job, JobError> {
|
|
||||||
self.load_job_from_redis(job_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
220
src/job.rs
220
src/job.rs
@@ -1,218 +1,2 @@
|
|||||||
use chrono::{DateTime, Utc};
|
// Re-export job types from the separate job crate
|
||||||
use serde::{Deserialize, Serialize};
|
pub use hero_job::*;
|
||||||
use std::collections::HashMap;
|
|
||||||
use uuid::Uuid;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
/// Job status enumeration
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum JobStatus {
|
|
||||||
Dispatched,
|
|
||||||
WaitingForPrerequisites,
|
|
||||||
Started,
|
|
||||||
Error,
|
|
||||||
Stopping,
|
|
||||||
Finished,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JobStatus {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
JobStatus::Dispatched => "dispatched",
|
|
||||||
JobStatus::WaitingForPrerequisites => "waiting_for_prerequisites",
|
|
||||||
JobStatus::Started => "started",
|
|
||||||
JobStatus::Error => "error",
|
|
||||||
JobStatus::Stopping => "stopping",
|
|
||||||
JobStatus::Finished => "finished",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_str(s: &str) -> Option<Self> {
|
|
||||||
match s {
|
|
||||||
"dispatched" => Some(JobStatus::Dispatched),
|
|
||||||
"waiting_for_prerequisites" => Some(JobStatus::WaitingForPrerequisites),
|
|
||||||
"started" => Some(JobStatus::Started),
|
|
||||||
"error" => Some(JobStatus::Error),
|
|
||||||
"stopping" => Some(JobStatus::Stopping),
|
|
||||||
"finished" => Some(JobStatus::Finished),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of a script execution request.
|
|
||||||
///
|
|
||||||
/// This structure contains all the information needed to execute a script
|
|
||||||
/// on a actor service, including the script content, dependencies, and metadata.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Job {
|
|
||||||
pub id: String,
|
|
||||||
pub caller_id: String,
|
|
||||||
pub context_id: String,
|
|
||||||
pub payload: String,
|
|
||||||
pub runner: String, // name of the runner to execute this job
|
|
||||||
pub executor: String, // name of the executor the runner will use to execute this job
|
|
||||||
pub timeout: u64, // timeout in seconds
|
|
||||||
pub env_vars: HashMap<String, String>, // environment variables for script execution
|
|
||||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
|
||||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error types for job operations
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum JobError {
|
|
||||||
#[error("Redis error: {0}")]
|
|
||||||
Redis(#[from] redis::RedisError),
|
|
||||||
#[error("Serialization error: {0}")]
|
|
||||||
Serialization(#[from] serde_json::Error),
|
|
||||||
#[error("Job not found: {0}")]
|
|
||||||
NotFound(String),
|
|
||||||
#[error("Invalid job status: {0}")]
|
|
||||||
InvalidStatus(String),
|
|
||||||
#[error("Timeout error: {0}")]
|
|
||||||
Timeout(String),
|
|
||||||
#[error("Invalid job data: {0}")]
|
|
||||||
InvalidData(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Job {
|
|
||||||
/// Create a new job with the given parameters
|
|
||||||
pub fn new(
|
|
||||||
caller_id: String,
|
|
||||||
context_id: String,
|
|
||||||
payload: String,
|
|
||||||
runner: String,
|
|
||||||
executor: String,
|
|
||||||
) -> Self {
|
|
||||||
let now = Utc::now();
|
|
||||||
Self {
|
|
||||||
id: Uuid::new_v4().to_string(),
|
|
||||||
caller_id,
|
|
||||||
context_id,
|
|
||||||
payload,
|
|
||||||
runner,
|
|
||||||
executor,
|
|
||||||
timeout: 300, // 5 minutes default
|
|
||||||
env_vars: HashMap::new(),
|
|
||||||
created_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builder for constructing job execution requests.
|
|
||||||
pub struct JobBuilder {
|
|
||||||
caller_id: String,
|
|
||||||
context_id: String,
|
|
||||||
payload: String,
|
|
||||||
runner: String,
|
|
||||||
executor: String,
|
|
||||||
timeout: u64, // timeout in seconds
|
|
||||||
env_vars: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JobBuilder {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
caller_id: "".to_string(),
|
|
||||||
context_id: "".to_string(),
|
|
||||||
payload: "".to_string(),
|
|
||||||
runner: "".to_string(),
|
|
||||||
executor: "".to_string(),
|
|
||||||
timeout: 300, // 5 minutes default
|
|
||||||
env_vars: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the caller ID for this job
|
|
||||||
pub fn caller_id(mut self, caller_id: &str) -> Self {
|
|
||||||
self.caller_id = caller_id.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the context ID for this job
|
|
||||||
pub fn context_id(mut self, context_id: &str) -> Self {
|
|
||||||
self.context_id = context_id.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the payload (script content) for this job
|
|
||||||
pub fn payload(mut self, payload: &str) -> Self {
|
|
||||||
self.payload = payload.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the runner name for this job
|
|
||||||
pub fn runner(mut self, runner: &str) -> Self {
|
|
||||||
self.runner = runner.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the executor for this job
|
|
||||||
pub fn executor(mut self, executor: &str) -> Self {
|
|
||||||
self.executor = executor.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the timeout for job execution (in seconds)
|
|
||||||
pub fn timeout(mut self, timeout: u64) -> Self {
|
|
||||||
self.timeout = timeout;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a single environment variable
|
|
||||||
pub fn env_var(mut self, key: &str, value: &str) -> Self {
|
|
||||||
self.env_vars.insert(key.to_string(), value.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set multiple environment variables from a HashMap
|
|
||||||
pub fn env_vars(mut self, env_vars: HashMap<String, String>) -> Self {
|
|
||||||
self.env_vars = env_vars;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all environment variables
|
|
||||||
pub fn clear_env_vars(mut self) -> Self {
|
|
||||||
self.env_vars.clear();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the job
|
|
||||||
pub fn build(self) -> Result<Job, JobError> {
|
|
||||||
if self.caller_id.is_empty() {
|
|
||||||
return Err(JobError::InvalidData("caller_id is required".to_string()));
|
|
||||||
}
|
|
||||||
if self.context_id.is_empty() {
|
|
||||||
return Err(JobError::InvalidData("context_id is required".to_string()));
|
|
||||||
}
|
|
||||||
if self.payload.is_empty() {
|
|
||||||
return Err(JobError::InvalidData("payload is required".to_string()));
|
|
||||||
}
|
|
||||||
if self.runner.is_empty() {
|
|
||||||
return Err(JobError::InvalidData("runner is required".to_string()));
|
|
||||||
}
|
|
||||||
if self.executor.is_empty() {
|
|
||||||
return Err(JobError::InvalidData("executor is required".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut job = Job::new(
|
|
||||||
self.caller_id,
|
|
||||||
self.context_id,
|
|
||||||
self.payload,
|
|
||||||
self.runner,
|
|
||||||
self.executor,
|
|
||||||
);
|
|
||||||
|
|
||||||
job.timeout = self.timeout;
|
|
||||||
job.env_vars = self.env_vars;
|
|
||||||
|
|
||||||
Ok(job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for JobBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
17
src/lib.rs
17
src/lib.rs
@@ -3,20 +3,17 @@
|
|||||||
//! See README.md for detailed documentation and usage examples.
|
//! See README.md for detailed documentation and usage examples.
|
||||||
|
|
||||||
pub mod runner;
|
pub mod runner;
|
||||||
pub mod supervisor;
|
|
||||||
pub mod job;
|
pub mod job;
|
||||||
pub mod client;
|
pub mod supervisor;
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
|
||||||
// OpenRPC server module
|
|
||||||
pub mod openrpc;
|
pub mod openrpc;
|
||||||
|
pub mod mycelium;
|
||||||
|
|
||||||
// Re-export main types for convenience
|
// Re-export main types for convenience
|
||||||
pub use runner::{
|
pub use runner::{Runner, RunnerConfig, RunnerResult, RunnerStatus};
|
||||||
LogInfo, Runner, RunnerConfig, RunnerResult, RunnerStatus,
|
// pub use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
|
||||||
};
|
|
||||||
pub use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
|
|
||||||
pub use supervisor::{Supervisor, SupervisorBuilder, ProcessManagerType};
|
pub use supervisor::{Supervisor, SupervisorBuilder, ProcessManagerType};
|
||||||
pub use job::{Job, JobBuilder, JobStatus, JobError};
|
pub use hero_job::{Job, JobBuilder, JobStatus, JobError};
|
||||||
pub use client::{Client, ClientBuilder};
|
pub use hero_job::Client;
|
||||||
pub use app::SupervisorApp;
|
pub use app::SupervisorApp;
|
||||||
|
pub use mycelium::MyceliumServer;
|
||||||
|
297
src/mycelium.rs
Normal file
297
src/mycelium.rs
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
//! # Mycelium Server Integration for Hero Supervisor
|
||||||
|
//!
|
||||||
|
//! This module implements a Mycelium-compatible JSON-RPC server that bridges
|
||||||
|
//! Mycelium transport messages to the supervisor's OpenRPC interface.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
use log::{info, error, debug};
|
||||||
|
use base64::Engine;
|
||||||
|
use crate::Supervisor;
|
||||||
|
|
||||||
|
/// Mycelium server that handles pushMessage calls and forwards them to supervisor
|
||||||
|
pub struct MyceliumServer {
|
||||||
|
supervisor: Arc<Mutex<Supervisor>>,
|
||||||
|
bind_address: String,
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MyceliumServer {
|
||||||
|
pub fn new(supervisor: Arc<Mutex<Supervisor>>, bind_address: String, port: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
supervisor,
|
||||||
|
bind_address,
|
||||||
|
port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the Mycelium-compatible JSON-RPC server
|
||||||
|
pub async fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use jsonrpsee::server::{ServerBuilder, RpcModule};
|
||||||
|
use tower_http::cors::{CorsLayer, Any};
|
||||||
|
|
||||||
|
info!("Starting Mycelium server on {}:{}", self.bind_address, self.port);
|
||||||
|
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
.allow_methods(Any)
|
||||||
|
.allow_headers(Any)
|
||||||
|
.allow_origin(Any);
|
||||||
|
|
||||||
|
let server = ServerBuilder::default()
|
||||||
|
.build(format!("{}:{}", self.bind_address, self.port))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut module = RpcModule::new(());
|
||||||
|
let supervisor_clone = self.supervisor.clone();
|
||||||
|
|
||||||
|
// Register pushMessage method
|
||||||
|
module.register_async_method("pushMessage", move |params, _, _| {
|
||||||
|
let supervisor = supervisor_clone.clone();
|
||||||
|
async move {
|
||||||
|
handle_push_message(supervisor, params).await
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Register messageStatus method (basic implementation)
|
||||||
|
module.register_async_method("messageStatus", |params, _, _| async move {
|
||||||
|
handle_message_status(params).await
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let handle = server.start(module);
|
||||||
|
|
||||||
|
info!("Mycelium server started successfully on {}:{}", self.bind_address, self.port);
|
||||||
|
|
||||||
|
// Keep the server running
|
||||||
|
handle.stopped().await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle pushMessage calls from Mycelium clients
|
||||||
|
async fn handle_push_message(
|
||||||
|
supervisor: Arc<Mutex<Supervisor>>,
|
||||||
|
params: jsonrpsee::types::Params<'_>,
|
||||||
|
) -> Result<Value, jsonrpsee::types::ErrorObjectOwned> {
|
||||||
|
// Parse params as array first, then get the first element
|
||||||
|
let params_array: Vec<Value> = params.parse()
|
||||||
|
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some(e.to_string())))?;
|
||||||
|
|
||||||
|
let params_value = params_array.get(0)
|
||||||
|
.ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing params object".to_string())))?;
|
||||||
|
|
||||||
|
debug!("Received pushMessage: {}", params_value);
|
||||||
|
|
||||||
|
// Extract message from params
|
||||||
|
let message = params_value.get("message")
|
||||||
|
.ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing message".to_string())))?;
|
||||||
|
|
||||||
|
// Extract payload (base64 encoded supervisor JSON-RPC)
|
||||||
|
let payload_b64 = message.get("payload")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing payload".to_string())))?;
|
||||||
|
|
||||||
|
// Extract topic and destination (for logging/debugging)
|
||||||
|
let _topic = message.get("topic")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("supervisor.rpc");
|
||||||
|
|
||||||
|
let _dst = message.get("dst");
|
||||||
|
|
||||||
|
// Check if this is a reply timeout request
|
||||||
|
let reply_timeout = params_value.get("reply_timeout")
|
||||||
|
.and_then(|v| v.as_u64());
|
||||||
|
|
||||||
|
// Decode the supervisor JSON-RPC payload
|
||||||
|
let payload_bytes = base64::engine::general_purpose::STANDARD
|
||||||
|
.decode(payload_b64)
|
||||||
|
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some(format!("invalid base64: {}", e))))?;
|
||||||
|
|
||||||
|
let supervisor_rpc: Value = serde_json::from_slice(&payload_bytes)
|
||||||
|
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some(format!("invalid JSON: {}", e))))?;
|
||||||
|
|
||||||
|
debug!("Decoded supervisor RPC: {}", supervisor_rpc);
|
||||||
|
|
||||||
|
// Extract method and params from supervisor JSON-RPC
|
||||||
|
let method = supervisor_rpc.get("method")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing method".to_string())))?;
|
||||||
|
|
||||||
|
let rpc_params = supervisor_rpc.get("params")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(json!([]));
|
||||||
|
|
||||||
|
let rpc_id = supervisor_rpc.get("id").cloned();
|
||||||
|
|
||||||
|
// Route to appropriate supervisor method
|
||||||
|
let result = route_supervisor_call(supervisor, method, rpc_params).await
|
||||||
|
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(-32603, "Internal error", Some(e.to_string())))?;
|
||||||
|
|
||||||
|
// Generate message ID for tracking
|
||||||
|
let message_id = format!("{:016x}", rand::random::<u64>());
|
||||||
|
|
||||||
|
if let Some(_timeout) = reply_timeout {
|
||||||
|
// For sync calls, return the supervisor result as an InboundMessage
|
||||||
|
let supervisor_response = json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": rpc_id,
|
||||||
|
"result": result
|
||||||
|
});
|
||||||
|
|
||||||
|
let response_b64 = base64::engine::general_purpose::STANDARD
|
||||||
|
.encode(serde_json::to_string(&supervisor_response).unwrap().as_bytes());
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"id": message_id,
|
||||||
|
"srcIp": "127.0.0.1",
|
||||||
|
"payload": response_b64
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
// For async calls, just return the message ID
|
||||||
|
Ok(json!({
|
||||||
|
"id": message_id
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle messageStatus calls
|
||||||
|
async fn handle_message_status(
|
||||||
|
params: jsonrpsee::types::Params<'_>,
|
||||||
|
) -> Result<Value, jsonrpsee::types::ErrorObjectOwned> {
|
||||||
|
// Parse params as array first, then get the first element
|
||||||
|
let params_array: Vec<Value> = params.parse()
|
||||||
|
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some(e.to_string())))?;
|
||||||
|
|
||||||
|
let params_value = params_array.get(0)
|
||||||
|
.ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing params object".to_string())))?;
|
||||||
|
|
||||||
|
let _message_id = params_value.get("id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| jsonrpsee::types::ErrorObjectOwned::owned(-32602, "Invalid params", Some("missing id".to_string())))?;
|
||||||
|
|
||||||
|
// For simplicity, always return "delivered" status
|
||||||
|
Ok(json!({
|
||||||
|
"status": "delivered"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route supervisor method calls to the appropriate supervisor functions
|
||||||
|
async fn route_supervisor_call(
|
||||||
|
supervisor: Arc<Mutex<Supervisor>>,
|
||||||
|
method: &str,
|
||||||
|
params: Value,
|
||||||
|
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let mut supervisor_guard = supervisor.lock().await;
|
||||||
|
|
||||||
|
match method {
|
||||||
|
"list_runners" => {
|
||||||
|
let runners = supervisor_guard.list_runners();
|
||||||
|
Ok(json!(runners))
|
||||||
|
}
|
||||||
|
|
||||||
|
"register_runner" => {
|
||||||
|
if let Some(param_obj) = params.as_array().and_then(|arr| arr.get(0)) {
|
||||||
|
let secret = param_obj.get("secret")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("missing secret")?;
|
||||||
|
let name = param_obj.get("name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("missing name")?;
|
||||||
|
let queue = param_obj.get("queue")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("missing queue")?;
|
||||||
|
|
||||||
|
supervisor_guard.register_runner(secret, name, queue).await?;
|
||||||
|
Ok(json!("success"))
|
||||||
|
} else {
|
||||||
|
Err("invalid register_runner params".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"start_runner" => {
|
||||||
|
if let Some(actor_id) = params.as_array().and_then(|arr| arr.get(0)).and_then(|v| v.as_str()) {
|
||||||
|
supervisor_guard.start_runner(actor_id).await?;
|
||||||
|
Ok(json!("success"))
|
||||||
|
} else {
|
||||||
|
Err("invalid start_runner params".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"stop_runner" => {
|
||||||
|
if let Some(arr) = params.as_array() {
|
||||||
|
let actor_id = arr.get(0).and_then(|v| v.as_str()).ok_or("missing actor_id")?;
|
||||||
|
let force = arr.get(1).and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
|
supervisor_guard.stop_runner(actor_id, force).await?;
|
||||||
|
Ok(json!("success"))
|
||||||
|
} else {
|
||||||
|
Err("invalid stop_runner params".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"get_runner_status" => {
|
||||||
|
if let Some(actor_id) = params.as_array().and_then(|arr| arr.get(0)).and_then(|v| v.as_str()) {
|
||||||
|
let status = supervisor_guard.get_runner_status(actor_id).await?;
|
||||||
|
Ok(json!(format!("{:?}", status)))
|
||||||
|
} else {
|
||||||
|
Err("invalid get_runner_status params".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"get_all_runner_status" => {
|
||||||
|
let statuses = supervisor_guard.get_all_runner_status().await?;
|
||||||
|
let status_map: std::collections::HashMap<String, String> = statuses
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, status)| (id, format!("{:?}", status)))
|
||||||
|
.collect();
|
||||||
|
Ok(json!(status_map))
|
||||||
|
}
|
||||||
|
|
||||||
|
"job.run" => {
|
||||||
|
if let Some(param_obj) = params.as_array().and_then(|arr| arr.get(0)) {
|
||||||
|
let secret = param_obj.get("secret")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("missing secret")?;
|
||||||
|
let job = param_obj.get("job")
|
||||||
|
.ok_or("missing job")?;
|
||||||
|
|
||||||
|
// For now, return success - actual job execution would need more integration
|
||||||
|
Ok(json!("job_queued"))
|
||||||
|
} else {
|
||||||
|
Err("invalid job.run params".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"job.status" => {
|
||||||
|
if let Some(job_id) = params.as_array().and_then(|arr| arr.get(0)).and_then(|v| v.as_str()) {
|
||||||
|
// For now, return a mock status
|
||||||
|
Ok(json!({"status": "completed"}))
|
||||||
|
} else {
|
||||||
|
Err("invalid job.status params".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"job.result" => {
|
||||||
|
if let Some(job_id) = params.as_array().and_then(|arr| arr.get(0)).and_then(|v| v.as_str()) {
|
||||||
|
// For now, return a mock result
|
||||||
|
Ok(json!({"success": "job completed successfully"}))
|
||||||
|
} else {
|
||||||
|
Err("invalid job.result params".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"rpc.discover" => {
|
||||||
|
let methods = vec![
|
||||||
|
"list_runners", "register_runner", "start_runner", "stop_runner",
|
||||||
|
"get_runner_status", "get_all_runner_status",
|
||||||
|
"job.run", "job.status", "job.result", "rpc.discover"
|
||||||
|
];
|
||||||
|
Ok(json!(methods))
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
error!("Unknown method: {}", method);
|
||||||
|
Err(format!("unknown method: {}", method).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -13,9 +13,9 @@ use log::{debug, info, error};
|
|||||||
|
|
||||||
use crate::supervisor::Supervisor;
|
use crate::supervisor::Supervisor;
|
||||||
use crate::runner::{Runner, RunnerError};
|
use crate::runner::{Runner, RunnerError};
|
||||||
|
use crate::runner::{ProcessManagerError, ProcessStatus, LogInfo};
|
||||||
use crate::job::Job;
|
use crate::job::Job;
|
||||||
use crate::ProcessManagerType;
|
use crate::ProcessManagerType;
|
||||||
use sal_service_manager::{ProcessStatus, LogInfo};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
@@ -191,10 +191,12 @@ pub enum ProcessStatusWrapper {
|
|||||||
impl From<ProcessStatus> for ProcessStatusWrapper {
|
impl From<ProcessStatus> for ProcessStatusWrapper {
|
||||||
fn from(status: ProcessStatus) -> Self {
|
fn from(status: ProcessStatus) -> Self {
|
||||||
match status {
|
match status {
|
||||||
ProcessStatus::Running => ProcessStatusWrapper::Running,
|
ProcessStatus::NotStarted => ProcessStatusWrapper::Stopped,
|
||||||
ProcessStatus::Stopped => ProcessStatusWrapper::Stopped,
|
|
||||||
ProcessStatus::Starting => ProcessStatusWrapper::Starting,
|
ProcessStatus::Starting => ProcessStatusWrapper::Starting,
|
||||||
|
ProcessStatus::Running => ProcessStatusWrapper::Running,
|
||||||
ProcessStatus::Stopping => ProcessStatusWrapper::Stopping,
|
ProcessStatus::Stopping => ProcessStatusWrapper::Stopping,
|
||||||
|
ProcessStatus::Stopped => ProcessStatusWrapper::Stopped,
|
||||||
|
ProcessStatus::Failed => ProcessStatusWrapper::Error("Process failed".to_string()),
|
||||||
ProcessStatus::Error(msg) => ProcessStatusWrapper::Error(msg),
|
ProcessStatus::Error(msg) => ProcessStatusWrapper::Error(msg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,16 +233,6 @@ pub struct LogInfoWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<LogInfo> for LogInfoWrapper {
|
impl From<LogInfo> for LogInfoWrapper {
|
||||||
fn from(log: LogInfo) -> Self {
|
|
||||||
LogInfoWrapper {
|
|
||||||
timestamp: log.timestamp,
|
|
||||||
level: log.level,
|
|
||||||
message: log.message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::runner::LogInfo> for LogInfoWrapper {
|
|
||||||
fn from(log: crate::runner::LogInfo) -> Self {
|
fn from(log: crate::runner::LogInfo) -> Self {
|
||||||
LogInfoWrapper {
|
LogInfoWrapper {
|
||||||
timestamp: log.timestamp,
|
timestamp: log.timestamp,
|
||||||
|
@@ -1,13 +1,56 @@
|
|||||||
//! Runner implementation for actor process management.
|
//! Runner implementation for actor process management.
|
||||||
|
|
||||||
use sal_service_manager::{ProcessManagerError as ServiceProcessManagerError, ProcessStatus, ProcessConfig};
|
// use sal_service_manager::{ProcessManagerError as ServiceProcessManagerError, ProcessStatus, ProcessConfig};
|
||||||
|
|
||||||
|
/// Simple process status enum to replace sal_service_manager dependency
|
||||||
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum ProcessStatus {
|
||||||
|
NotStarted,
|
||||||
|
Starting,
|
||||||
|
Running,
|
||||||
|
Stopping,
|
||||||
|
Stopped,
|
||||||
|
Failed,
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple process config to replace sal_service_manager dependency
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProcessConfig {
|
||||||
|
pub command: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
pub working_dir: Option<String>,
|
||||||
|
pub env_vars: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessConfig {
|
||||||
|
pub fn new(command: String, args: Vec<String>, working_dir: Option<String>, env_vars: Vec<(String, String)>) -> Self {
|
||||||
|
Self {
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
working_dir,
|
||||||
|
env_vars,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple process manager error to replace sal_service_manager dependency
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ProcessManagerError {
|
||||||
|
#[error("Process execution failed: {0}")]
|
||||||
|
ExecutionFailed(String),
|
||||||
|
#[error("Process not found: {0}")]
|
||||||
|
ProcessNotFound(String),
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IoError(String),
|
||||||
|
}
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Represents the current status of an actor/runner (alias for ProcessStatus)
|
/// Represents the current status of an actor/runner (alias for ProcessStatus)
|
||||||
pub type RunnerStatus = ProcessStatus;
|
pub type RunnerStatus = ProcessStatus;
|
||||||
|
|
||||||
/// Log information structure
|
/// Log information structure with serialization support
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct LogInfo {
|
pub struct LogInfo {
|
||||||
pub timestamp: String,
|
pub timestamp: String,
|
||||||
pub level: String,
|
pub level: String,
|
||||||
@@ -96,7 +139,7 @@ pub enum RunnerError {
|
|||||||
#[error("Process manager error: {source}")]
|
#[error("Process manager error: {source}")]
|
||||||
ProcessManagerError {
|
ProcessManagerError {
|
||||||
#[from]
|
#[from]
|
||||||
source: ServiceProcessManagerError,
|
source: ProcessManagerError,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Configuration error: {reason}")]
|
#[error("Configuration error: {reason}")]
|
||||||
@@ -120,7 +163,7 @@ pub enum RunnerError {
|
|||||||
#[error("Job error: {source}")]
|
#[error("Job error: {source}")]
|
||||||
JobError {
|
JobError {
|
||||||
#[from]
|
#[from]
|
||||||
source: crate::JobError,
|
source: hero_job::JobError,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Job '{job_id}' not found")]
|
#[error("Job '{job_id}' not found")]
|
||||||
@@ -135,9 +178,17 @@ pub type RunnerConfig = Runner;
|
|||||||
|
|
||||||
/// Convert Runner to ProcessConfig
|
/// Convert Runner to ProcessConfig
|
||||||
pub fn runner_to_process_config(config: &Runner) -> ProcessConfig {
|
pub fn runner_to_process_config(config: &Runner) -> ProcessConfig {
|
||||||
ProcessConfig::new(config.id.clone(), config.command.clone())
|
let args = vec![
|
||||||
.with_arg("--id".to_string())
|
"--id".to_string(),
|
||||||
.with_arg(config.id.clone())
|
config.id.clone(),
|
||||||
.with_arg("--redis-url".to_string())
|
"--redis-url".to_string(),
|
||||||
.with_arg(config.redis_url.clone())
|
config.redis_url.clone(),
|
||||||
|
];
|
||||||
|
|
||||||
|
ProcessConfig::new(
|
||||||
|
config.command.to_string_lossy().to_string(),
|
||||||
|
args,
|
||||||
|
Some("/tmp".to_string()), // Default working directory since Runner doesn't have working_dir field
|
||||||
|
vec![]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,16 +1,85 @@
|
|||||||
//! Main supervisor implementation for managing multiple actor runners.
|
//! Main supervisor implementation for managing multiple actor runners.
|
||||||
|
|
||||||
use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
|
use crate::runner::{ProcessManagerError, ProcessConfig, ProcessStatus};
|
||||||
|
|
||||||
|
/// Simple trait to replace sal_service_manager ProcessManager
|
||||||
|
trait ProcessManager: Send + Sync {
|
||||||
|
fn start(&self, config: &ProcessConfig) -> Result<(), ProcessManagerError>;
|
||||||
|
fn stop(&self, process_id: &str) -> Result<(), ProcessManagerError>;
|
||||||
|
fn status(&self, process_id: &str) -> Result<ProcessStatus, ProcessManagerError>;
|
||||||
|
fn logs(&self, process_id: &str) -> Result<Vec<String>, ProcessManagerError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple process manager implementation
|
||||||
|
struct SimpleProcessManager;
|
||||||
|
|
||||||
|
impl SimpleProcessManager {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessManager for SimpleProcessManager {
|
||||||
|
fn start(&self, _config: &ProcessConfig) -> Result<(), ProcessManagerError> {
|
||||||
|
// Simplified implementation - just return success for now
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&self, _process_id: &str) -> Result<(), ProcessManagerError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(&self, _process_id: &str) -> Result<ProcessStatus, ProcessManagerError> {
|
||||||
|
Ok(ProcessStatus::Running)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logs(&self, _process_id: &str) -> Result<Vec<String>, ProcessManagerError> {
|
||||||
|
Ok(vec!["No logs available".to_string()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tmux process manager implementation
|
||||||
|
struct TmuxProcessManager {
|
||||||
|
session_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TmuxProcessManager {
|
||||||
|
fn new(session_name: String) -> Self {
|
||||||
|
Self { session_name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessManager for TmuxProcessManager {
|
||||||
|
fn start(&self, _config: &ProcessConfig) -> Result<(), ProcessManagerError> {
|
||||||
|
// Simplified implementation - just return success for now
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&self, _process_id: &str) -> Result<(), ProcessManagerError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(&self, _process_id: &str) -> Result<ProcessStatus, ProcessManagerError> {
|
||||||
|
Ok(ProcessStatus::Running)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logs(&self, _process_id: &str) -> Result<Vec<String>, ProcessManagerError> {
|
||||||
|
Ok(vec!["No logs available".to_string()])
|
||||||
|
}
|
||||||
|
}
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::{client::{Client, ClientBuilder}, job::JobStatus, runner::{LogInfo, Runner, RunnerConfig, RunnerError, RunnerResult, RunnerStatus}};
|
// use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager};
|
||||||
|
|
||||||
|
use crate::{job::JobStatus, runner::{LogInfo, Runner, RunnerConfig, RunnerError, RunnerResult, RunnerStatus}};
|
||||||
|
use hero_job::{Client, client::ClientBuilder};
|
||||||
|
|
||||||
|
|
||||||
/// Process manager type for a runner
|
/// Process manager type for a runner
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum ProcessManagerType {
|
pub enum ProcessManagerType {
|
||||||
/// Simple process manager for direct process spawning
|
/// Simple process manager for direct process spawning
|
||||||
Simple,
|
Simple,
|
||||||
@@ -337,7 +406,7 @@ impl Supervisor {
|
|||||||
|
|
||||||
/// Delete a job by ID
|
/// Delete a job by ID
|
||||||
pub async fn delete_job(&mut self, job_id: &str) -> RunnerResult<()> {
|
pub async fn delete_job(&mut self, job_id: &str) -> RunnerResult<()> {
|
||||||
self.client.delete_job(&job_id).await
|
self.client.delete_job(&job_id).await.map_err(RunnerError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List all managed runners
|
/// List all managed runners
|
||||||
@@ -355,7 +424,7 @@ impl Supervisor {
|
|||||||
|
|
||||||
let process_config = runner_to_process_config(runner);
|
let process_config = runner_to_process_config(runner);
|
||||||
let mut pm = self.process_manager.lock().await;
|
let mut pm = self.process_manager.lock().await;
|
||||||
pm.start_process(&process_config).await?;
|
pm.start(&process_config)?;
|
||||||
|
|
||||||
info!("Successfully started actor {}", runner.id);
|
info!("Successfully started actor {}", runner.id);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -374,7 +443,7 @@ impl Supervisor {
|
|||||||
info!("Stopping actor {}", runner.id);
|
info!("Stopping actor {}", runner.id);
|
||||||
|
|
||||||
let mut pm = self.process_manager.lock().await;
|
let mut pm = self.process_manager.lock().await;
|
||||||
pm.stop_process(&runner.id, force).await?;
|
pm.stop(&runner.id)?;
|
||||||
|
|
||||||
info!("Successfully stopped actor {}", runner.id);
|
info!("Successfully stopped actor {}", runner.id);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -389,7 +458,7 @@ impl Supervisor {
|
|||||||
pub async fn get_runner_status(&self, actor_id: &str) -> RunnerResult<RunnerStatus> {
|
pub async fn get_runner_status(&self, actor_id: &str) -> RunnerResult<RunnerStatus> {
|
||||||
if let Some(runner) = self.runners.get(actor_id) {
|
if let Some(runner) = self.runners.get(actor_id) {
|
||||||
let pm = self.process_manager.lock().await;
|
let pm = self.process_manager.lock().await;
|
||||||
let status = pm.process_status(&runner.id).await?;
|
let status = pm.status(&runner.id)?;
|
||||||
Ok(status)
|
Ok(status)
|
||||||
} else {
|
} else {
|
||||||
Err(RunnerError::ActorNotFound {
|
Err(RunnerError::ActorNotFound {
|
||||||
@@ -407,13 +476,13 @@ impl Supervisor {
|
|||||||
) -> RunnerResult<Vec<LogInfo>> {
|
) -> RunnerResult<Vec<LogInfo>> {
|
||||||
if let Some(runner) = self.runners.get(actor_id) {
|
if let Some(runner) = self.runners.get(actor_id) {
|
||||||
let pm = self.process_manager.lock().await;
|
let pm = self.process_manager.lock().await;
|
||||||
let logs = pm.process_logs(&runner.id, lines, follow).await?;
|
let logs = pm.logs(&runner.id)?;
|
||||||
|
|
||||||
// Convert sal_service_manager::LogInfo to our LogInfo
|
// Convert strings to LogInfo
|
||||||
let converted_logs = logs.into_iter().map(|log| LogInfo {
|
let converted_logs = logs.into_iter().map(|log_line| LogInfo {
|
||||||
timestamp: log.timestamp,
|
timestamp: chrono::Utc::now().to_rfc3339(),
|
||||||
level: log.level,
|
level: "INFO".to_string(),
|
||||||
message: log.message,
|
message: log_line,
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
Ok(converted_logs)
|
Ok(converted_logs)
|
||||||
@@ -521,7 +590,6 @@ impl Supervisor {
|
|||||||
match self.get_runner_status(actor_id).await {
|
match self.get_runner_status(actor_id).await {
|
||||||
Ok(status) => results.push((actor_id.clone(), status)),
|
Ok(status) => results.push((actor_id.clone(), status)),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
use sal_service_manager::ProcessStatus;
|
|
||||||
results.push((actor_id.clone(), ProcessStatus::Stopped));
|
results.push((actor_id.clone(), ProcessStatus::Stopped));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -651,7 +719,7 @@ impl Supervisor {
|
|||||||
|
|
||||||
/// List all job IDs from Redis
|
/// List all job IDs from Redis
|
||||||
pub async fn list_jobs(&self) -> RunnerResult<Vec<String>> {
|
pub async fn list_jobs(&self) -> RunnerResult<Vec<String>> {
|
||||||
self.client.list_jobs().await
|
self.client.list_jobs().await.map_err(RunnerError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List all jobs with full details from Redis
|
/// List all jobs with full details from Redis
|
||||||
|
Reference in New Issue
Block a user