Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ on:
release:
types: [published]
workflow_dispatch:
inputs:
publish:
description: 'Publish to PyPI'
type: boolean
default: false

permissions:
contents: read
Expand Down Expand Up @@ -110,6 +115,7 @@ jobs:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: [linux, macos, sdist]
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.publish == true)
steps:
- uses: actions/download-artifact@v4
with:
Expand Down
74 changes: 71 additions & 3 deletions probing/server/src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ use axum::response::IntoResponse;
use bytes::Bytes;
use include_dir::include_dir;
use include_dir::Dir;
use once_cell::sync::Lazy;

static BASE_PATH: Lazy<String> = Lazy::new(|| {
env::var("PROBING_BASE_PATH")
.unwrap_or_default()
.trim_end_matches('/')
.to_string()
});

static ASSET: Dir = include_dir!("web/dist");

Expand Down Expand Up @@ -49,7 +57,65 @@ fn get_content_type(path: &str) -> &'static str {

/// Handler for index page
pub async fn index() -> impl IntoResponse {
([(header::CONTENT_TYPE, "text/html")], get("/index.html"))
let mut html = String::from_utf8_lossy(&get("/index.html")).to_string();
let base_path = BASE_PATH.clone();
if !base_path.is_empty() {
// Inject JS global for the frontend runtime
let inject = format!(
r#"<script>window.__PROBING_BASE_PATH__ = "{}";</script>"#,
base_path
);
// Intercept fetch to rewrite WASM URLs with base path prefix
let fetch_intercept = [
"<script>(function(){var bp=\"",
&base_path,
"\";var o=window.fetch;window.fetch=function(i,n){if(typeof i==='string'&&i.startsWith('/'))i=bp+i;return o.call(this,i,n)}})();</script>",
].concat();
let replacement = format!("<head>{}{}", inject, fetch_intercept);
html = html.replacen("<head>", &replacement, 1);

// Rewrite absolute paths in HTML to include base path prefix
html = rewrite_html_paths(&html, &base_path);
}
([(header::CONTENT_TYPE, "text/html")], html)
}

/// Rewrite absolute paths (src="/...", href="/...") in HTML to include base_path prefix.
/// Skips external URLs (//, http://, https://).
fn rewrite_html_paths(html: &str, base_path: &str) -> String {
let mut result = html.to_string();
for attr in &["src", "href"] {
let mut offset = 0;
let pattern = format!("{}=\"/", attr);
while let Some(pos) = result[offset..].find(&pattern) {
let global_pos = offset + pos;
let value_start = global_pos + pattern.len(); // position right after the leading /
// Find the closing quote to get the full attribute value
let value_end = result[value_start..]
.find('"')
.map(|i| value_start + i)
.unwrap_or(result.len());
let value = &result[value_start..value_end];
// Skip protocol-relative URLs: //cdn.example.com/...
if value.starts_with('/') {
offset = global_pos + 1;
continue;
}
// Skip external URLs: http:// or https://
if value.starts_with("http://") || value.starts_with("https://") {
offset = global_pos + 1;
continue;
}
// Insert base_path right after the leading /
// e.g. href="/./assets/foo.js" -> href="/proxy/task-123/./assets/foo.js"
result.insert_str(
value_start,
&format!("{}/", base_path.trim_start_matches('/')),
);
offset = value_start + base_path.len() + 1;
}
}
result
}

/// Handler for serving static files
Expand All @@ -59,6 +125,8 @@ pub async fn static_files(uri: Uri) -> Result<impl IntoResponse, StatusCode> {
return Err(StatusCode::NOT_FOUND);
}

log::debug!("serving file: {path}");
Ok(([(header::CONTENT_TYPE, get_content_type(path))], get(path)))
let content_type = get_content_type(path);
let data = get(path);

Ok(([(header::CONTENT_TYPE, content_type)], data))
}
1 change: 1 addition & 0 deletions probing/server/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ pub fn sync_env_settings() {
"PROBING_ASSETS_ROOT",
"PROBING_SERVER_ADDRPATTERN",
"PROBING_AUTH_TOKEN", // Skip syncing the auth token for security reasons
"PROBING_BASE_PATH", // Used by server at startup, not a runtime setting
]
.contains(&k.as_str())
})
Expand Down
1 change: 1 addition & 0 deletions web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ thiserror = "1.0"
# Logging
log = "0.4"
web-sys = { version = "0.3", features = ["Window", "Storage", "HtmlElement"] }
wasm-bindgen = "0.2"

# Icons
icondata = { version = "0.5.0", default-features = false, features = [
Expand Down
7 changes: 6 additions & 1 deletion web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Probing Web Interface</title>
<link rel="icon" type="image/svg" href="/logo.svg">
<link rel="icon" type="image/svg" id="favicon" href="/logo.svg">
<script>
if (window.__PROBING_BASE_PATH__) {
document.getElementById('favicon').href = window.__PROBING_BASE_PATH__ + '/logo.svg';
}
</script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
Expand Down
4 changes: 3 additions & 1 deletion web/src/api/analytics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ impl ApiClient {

match msg.payload {
QueryDataFormat::DataFrame(dataframe) => Ok(dataframe),
_ => Err(AppError::Api("Bad Response: DataFrame is Expected.".to_string()))
QueryDataFormat::Nil => Ok(DataFrame { names: vec![], cols: vec![], size: 0 }),
QueryDataFormat::Error(err) => Err(AppError::Api(err.message)),
QueryDataFormat::TimeSeries(_) => Err(AppError::Api("TimeSeries format not supported".to_string())),
}
}

Expand Down
2 changes: 1 addition & 1 deletion web/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ impl ApiClient {

/// Build API URL
fn build_url(path: &str) -> Result<String> {
Ok(format!("{}{}", Self::get_origin()?, path))
Ok(format!("{}{}", Self::get_origin()?, crate::utils::base_path::with_base(path)))
}

/// Send GET request
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/callstack_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub fn CallStackView(callstack: CallFrame) -> Element {
}
}
CallFrame::PyFrame { file, func, lineno, locals } => {
let url = format!("/apis/files?path={}", file);
let url = crate::utils::base_path::with_base(&format!("/apis/files?path={}", file));
let key = format!("{func} @ {file}: {lineno}");
rsx! {
CollapsibleCardWithIcon {
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/sidebar/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub fn Sidebar() -> Element {
Link {
to: Route::DashboardPage {},
class: "flex items-center gap-2",
img { src: "/assets/logo.svg", alt: "Probing", class: "w-7 h-7 flex-shrink-0" }
img { src: "{crate::utils::base_path::with_base(\"/assets/logo.svg\")}", alt: "Probing", class: "w-7 h-7 flex-shrink-0" }
span { class: "{brand}", "Probing" }
}
}
Expand Down
11 changes: 10 additions & 1 deletion web/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ mod state;
mod utils;

use app::App;
use utils::base_path::base_path;

fn main() {
launch(App);
let base = base_path();
if base.is_empty() {
launch(App);
} else {
let prefix = Some(base);
let config = dioxus_web::Config::new()
.history(std::rc::Rc::new(dioxus_web::WebHistory::new(prefix, true)));
dioxus_web::launch::launch_cfg(App, config);
}
}
22 changes: 22 additions & 0 deletions web/src/utils/base_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// Read the base path injected by the server into index.html.
///
/// When probing is deployed behind a reverse proxy with a sub-path prefix
/// (e.g. `/proxy/task-123`), the server sets `window.__PROBING_BASE_PATH__`
/// so the frontend can build correct URLs.
pub fn base_path() -> String {
#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#"
export function get_base_path() {
return window.__PROBING_BASE_PATH__ || "";
}
"#)]
extern "C" {
fn get_base_path() -> String;
}
get_base_path()
}

/// Prepend the base path to an absolute path (e.g. `/apis/nodes` → `/proxy/xxx/apis/nodes`).
pub fn with_base(path: &str) -> String {
let base = base_path();
format!("{}{}", base.trim_end_matches('/'), path)
}
1 change: 1 addition & 0 deletions web/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod base_path;
pub mod error;
pub mod tracing_viewer;
Loading