add file browser component and widget
This commit is contained in:
parent
4e43c21b72
commit
ba43a82db0
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/target
|
||||
.env
|
||||
target
|
||||
.env
|
||||
dist
|
1033
Cargo.lock
generated
1033
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
80
Cargo.toml
80
Cargo.toml
@ -1,52 +1,78 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"components",
|
||||
"widgets/file_browser_widget",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[package]
|
||||
name = "framework"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
[[bin]]
|
||||
name = "framework"
|
||||
path = "src/lib.rs"
|
||||
path = "cmd/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# WebSocket client dependency with conditional crypto features
|
||||
circle_client_ws = { path = "../circles/src/client_ws", default-features = false, features = [] }
|
||||
|
||||
# Core dependencies
|
||||
[workspace.dependencies]
|
||||
# Shared dependencies across workspace
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
log = "0.4"
|
||||
thiserror = "1.0"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
|
||||
# Async dependencies
|
||||
uuid = { version = "1.0", features = ["v4", "js"] }
|
||||
futures-util = "0.3"
|
||||
futures-channel = "0.3"
|
||||
|
||||
# WASM-specific dependencies
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
yew = { version = "0.21", features = ["csr"] }
|
||||
gloo = "0.11"
|
||||
gloo-timers = { version = "0.3", features = ["futures"] }
|
||||
web-sys = { version = "0.3", features = ["Storage", "Window", "FormData", "HtmlFormElement", "HtmlInputElement", "HtmlSelectElement"] }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"Storage", "Window", "FormData", "HtmlFormElement", "HtmlInputElement", "HtmlSelectElement",
|
||||
"Request", "RequestInit", "RequestMode", "Response", "Headers", "Blob", "Url",
|
||||
"HtmlAnchorElement", "Document", "Element", "CssStyleDeclaration", "Location",
|
||||
"UrlSearchParams", "HtmlTextAreaElement", "HtmlButtonElement", "HtmlDivElement",
|
||||
"Event", "EventTarget", "MouseEvent", "KeyboardEvent", "InputEvent",
|
||||
"File", "FileList", "AbortController", "AbortSignal", "console", "History",
|
||||
"HtmlImageElement", "HtmlCanvasElement", "Navigator", "Notification",
|
||||
"WebSocket", "MessageEvent", "CloseEvent", "ErrorEvent",
|
||||
"DomException", "Performance", "HtmlMetaElement", "HtmlLinkElement",
|
||||
"Node", "NodeList", "HtmlCollection", "DomTokenList",
|
||||
"CustomEvent", "FocusEvent", "WheelEvent", "TouchEvent", "Touch", "TouchList",
|
||||
"DragEvent", "DataTransfer", "ClipboardEvent",
|
||||
"HtmlIFrameElement", "MessagePort"
|
||||
] }
|
||||
js-sys = "0.3"
|
||||
hex = "0.4"
|
||||
k256 = { version = "0.13", features = ["ecdsa", "sha256"] }
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
# Native-specific dependencies
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1.0", features = ["rt", "macros", "time"] }
|
||||
# HTTP client
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
# Tokio for async runtime
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
|
||||
# Axum for web server
|
||||
axum = "0.7"
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["fs", "cors"] }
|
||||
|
||||
# Internal workspace dependencies
|
||||
components = { path = "components" }
|
||||
|
||||
[dependencies]
|
||||
# Use workspace dependencies for the binary
|
||||
components = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
env_logger = "0.11"
|
||||
|
||||
# Features
|
||||
[features]
|
||||
default = []
|
||||
crypto = ["circle_client_ws/crypto"]
|
||||
wasm-compatible = [] # For WASM builds without crypto to avoid wasm-opt issues
|
||||
|
||||
[workspace]
|
||||
members = ["examples/website"]
|
||||
|
@ -1,4 +1,8 @@
|
||||
# Framework WebSocket Connection Manager
|
||||
# Awesome Web Assembly for Real-time Experiences
|
||||
|
||||
This repository contains a collection of tools and libraries for building real-time experiences using WebAssembly (WASM). It includes a WebSocket connection manager built on top of the robust `circle_client_ws` library, as well as a file browser component for managing files on a server.
|
||||
|
||||
## Framework WebSocket Connection Manager
|
||||
|
||||
A simplified WebSocket connection manager built on top of the robust `circle_client_ws` library. This framework provides a clean builder pattern API for managing multiple self-managing WebSocket connections with authentication support and script execution capabilities.
|
||||
|
||||
|
@ -1,128 +0,0 @@
|
||||
# WebSocket Framework - WASM-opt Compatibility Solution
|
||||
|
||||
## Problem
|
||||
|
||||
The WebSocket connection manager framework was causing wasm-opt parsing errors when building for WASM targets with aggressive optimizations:
|
||||
|
||||
```
|
||||
[parse exception: invalid code after misc prefix: 17 (at 0:732852)]
|
||||
Fatal: error parsing wasm (try --debug for more info)
|
||||
```
|
||||
|
||||
## Root Cause
|
||||
|
||||
The issue was caused by cryptographic dependencies (`secp256k1` and `sha3`) in the `circle_client_ws` library. These libraries contain complex low-level implementations that are incompatible with wasm-opt's aggressive optimization passes.
|
||||
|
||||
## Solution
|
||||
|
||||
We implemented a feature flag system that allows the framework to work in two modes:
|
||||
|
||||
### 1. Full Mode (with crypto authentication)
|
||||
- **Use case**: Native applications, server-side usage
|
||||
- **Features**: Full secp256k1 authentication support
|
||||
- **Usage**: `framework = { path = "...", features = ["crypto"] }`
|
||||
|
||||
### 2. WASM-Compatible Mode (without crypto)
|
||||
- **Use case**: WASM/browser applications where wasm-opt compatibility is required
|
||||
- **Features**: Basic WebSocket connections without cryptographic authentication
|
||||
- **Usage**: `framework = { path = "...", features = ["wasm-compatible"] }`
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Framework Cargo.toml
|
||||
```toml
|
||||
[dependencies]
|
||||
circle_client_ws = { path = "../circles/src/client_ws", default-features = false, features = [] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
crypto = ["circle_client_ws/crypto"]
|
||||
wasm-compatible = [] # For WASM builds without crypto to avoid wasm-opt issues
|
||||
```
|
||||
|
||||
### Conditional Compilation
|
||||
The authentication code is conditionally compiled based on feature flags:
|
||||
|
||||
```rust
|
||||
#[cfg(feature = "crypto")]
|
||||
pub fn create_client(&self, ws_url: String) -> circle_client_ws::CircleWsClient {
|
||||
circle_client_ws::CircleWsClientBuilder::new(ws_url)
|
||||
.with_keypair(self.private_key.clone())
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "crypto"))]
|
||||
pub fn create_client(&self, ws_url: String) -> circle_client_ws::CircleWsClient {
|
||||
circle_client_ws::CircleWsClientBuilder::new(ws_url).build()
|
||||
}
|
||||
```
|
||||
|
||||
### Website Example Configuration
|
||||
```toml
|
||||
[dependencies]
|
||||
framework = { path = "../..", features = ["wasm-compatible"] }
|
||||
```
|
||||
|
||||
## Usage Recommendations
|
||||
|
||||
### For WASM Applications
|
||||
Use the `wasm-compatible` feature to avoid wasm-opt issues:
|
||||
```toml
|
||||
framework = { features = ["wasm-compatible"] }
|
||||
```
|
||||
|
||||
### For Native Applications with Authentication
|
||||
Use the `crypto` feature for full authentication support:
|
||||
```toml
|
||||
framework = { features = ["crypto"] }
|
||||
```
|
||||
|
||||
### For Development/Testing
|
||||
You can disable wasm-opt entirely in Trunk.toml for development:
|
||||
```toml
|
||||
[tools]
|
||||
wasm-opt = false
|
||||
```
|
||||
|
||||
## Alternative Solutions Considered
|
||||
|
||||
1. **Less aggressive wasm-opt settings**: Tried `-O2` instead of `-Os`, but still failed
|
||||
2. **Disabling specific wasm-opt passes**: Complex and unreliable
|
||||
3. **Different crypto libraries**: Would require significant changes to circle_client_ws
|
||||
4. **WASM-specific crypto implementations**: Would add complexity and maintenance burden
|
||||
|
||||
## Benefits of This Solution
|
||||
|
||||
1. **Backward Compatibility**: Existing native applications continue to work unchanged
|
||||
2. **WASM Compatibility**: Browser applications can use the framework without wasm-opt issues
|
||||
3. **Clear Separation**: Feature flags make the trade-offs explicit
|
||||
4. **Maintainable**: Simple conditional compilation without code duplication
|
||||
5. **Future-Proof**: Can easily add more features or modes as needed
|
||||
|
||||
## Testing
|
||||
|
||||
The solution was verified by:
|
||||
1. Building the website example without framework dependency ✅
|
||||
2. Adding framework dependency without crypto features ✅
|
||||
3. Building with wasm-opt aggressive optimizations ✅
|
||||
4. Confirming all functionality works in WASM-compatible mode ✅
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Existing Native Applications
|
||||
No changes required - continue using the framework as before.
|
||||
|
||||
### New WASM Applications
|
||||
Add the `wasm-compatible` feature:
|
||||
```toml
|
||||
framework = { features = ["wasm-compatible"] }
|
||||
```
|
||||
|
||||
### Applications Needing Both
|
||||
Use conditional dependencies:
|
||||
```toml
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
framework = { features = ["wasm-compatible"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
framework = { features = ["crypto"] }
|
42
cmd/main.rs
Normal file
42
cmd/main.rs
Normal file
@ -0,0 +1,42 @@
|
||||
//! Framework Binary
|
||||
//!
|
||||
//! Main entry point for the framework application.
|
||||
|
||||
use components::prelude::*;
|
||||
use std::env;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize logging
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
println!("Framework v{}", VERSION);
|
||||
println!("Usage: framework <command>");
|
||||
println!();
|
||||
println!("Commands:");
|
||||
println!(" serve Start the web server");
|
||||
println!(" version Show version information");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match args[1].as_str() {
|
||||
"serve" => {
|
||||
println!("Starting framework server...");
|
||||
// TODO: Implement server functionality using components
|
||||
println!("Server functionality not yet implemented");
|
||||
}
|
||||
"version" => {
|
||||
println!("Framework v{}", VERSION);
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unknown command: {}", args[1]);
|
||||
eprintln!("Run 'framework' for usage information");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
48
components/Cargo.toml
Normal file
48
components/Cargo.toml
Normal file
@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "components"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "components"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
# Use workspace dependencies
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
futures-channel = { workspace = true }
|
||||
|
||||
# WASM-specific dependencies
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = { workspace = true }
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
yew = { workspace = true }
|
||||
gloo = { workspace = true }
|
||||
gloo-timers = { workspace = true }
|
||||
web-sys = { workspace = true }
|
||||
js-sys = { workspace = true }
|
||||
serde-wasm-bindgen = "0.6"
|
||||
hex = "0.4"
|
||||
k256 = { version = "0.13", features = ["ecdsa", "sha256"] }
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
gloo-net = "0.5"
|
||||
reqwest = { workspace = true }
|
||||
|
||||
# Native-specific dependencies
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
hero_websocket_client = { path = "../../hero/interfaces/websocket/client", default-features = false, features = [] }
|
||||
tokio = { workspace = true, features = ["rt", "macros", "time"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
wasm-bindgen-test = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
crypto = ["hero_websocket_client/crypto"]
|
||||
wasm-compatible = [] # For WASM builds without crypto to avoid wasm-opt issues
|
@ -97,9 +97,9 @@ impl AuthConfig {
|
||||
///
|
||||
/// # Returns
|
||||
/// A basic CircleWsClient without authentication
|
||||
#[cfg(not(feature = "crypto"))]
|
||||
pub fn create_client(&self, ws_url: String) -> circle_client_ws::CircleWsClient {
|
||||
circle_client_ws::CircleWsClientBuilder::new(ws_url).build()
|
||||
#[cfg(all(not(feature = "crypto"), not(target_arch = "wasm32")))]
|
||||
pub fn create_client(&self, ws_url: String) -> hero_websocket_client::CircleWsClient {
|
||||
hero_websocket_client::CircleWsClientBuilder::new(ws_url).build()
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,13 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use yew::prelude::*;
|
||||
use web_sys::Storage;
|
||||
use k256::{SecretKey, elliptic_curve::{rand_core::OsRng, sec1::ToEncodedPoint}};
|
||||
use k256::{SecretKey, elliptic_curve::sec1::ToEncodedPoint};
|
||||
use getrandom::getrandom;
|
||||
use crate::error::{WsError, WsResult};
|
||||
// Note: error module not available in WASM builds
|
||||
// use crate::error::{WsError, WsResult};
|
||||
|
||||
// Define local error types for WASM builds
|
||||
pub type WsResult<T> = Result<T, String>;
|
||||
|
||||
const STORAGE_KEY: &str = "herocode_auth_keys";
|
||||
|
||||
@ -113,22 +117,22 @@ impl BrowserAuthManager {
|
||||
AuthState::Authenticated { key_name: current_key, private_key } if current_key == key_name => {
|
||||
// Convert private key to public key
|
||||
let private_key_bytes = hex::decode(private_key)
|
||||
.map_err(|_| WsError::auth("Invalid private key hex"))?;
|
||||
.map_err(|_| "Invalid private key hex")?;
|
||||
|
||||
if private_key_bytes.len() != 32 {
|
||||
return Err(WsError::auth("Invalid private key length"));
|
||||
return Err("Invalid private key length".to_string());
|
||||
}
|
||||
|
||||
let mut key_array = [0u8; 32];
|
||||
key_array.copy_from_slice(&private_key_bytes);
|
||||
|
||||
let secret_key = SecretKey::from_bytes(&key_array.into())
|
||||
.map_err(|e| WsError::auth(&format!("Failed to create secret key: {}", e)))?;
|
||||
.map_err(|e| format!("Failed to create secret key: {}", e))?;
|
||||
|
||||
let public_key = secret_key.public_key();
|
||||
Ok(hex::encode(public_key.to_encoded_point(false).as_bytes()))
|
||||
}
|
||||
_ => Err(WsError::auth("Key not currently authenticated"))
|
||||
_ => Err("Key not currently authenticated".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +140,7 @@ impl BrowserAuthManager {
|
||||
pub fn register_key(&mut self, name: String, private_key: String, password: String) -> WsResult<()> {
|
||||
// Validate private key format
|
||||
if !self.validate_private_key(&private_key)? {
|
||||
return Err(WsError::auth("Invalid private key format"));
|
||||
return Err("Invalid private key format".to_string());
|
||||
}
|
||||
|
||||
// Generate a random salt
|
||||
@ -161,14 +165,14 @@ impl BrowserAuthManager {
|
||||
/// Attempt to login with a specific key and password
|
||||
pub fn login(&mut self, key_name: String, password: String) -> WsResult<()> {
|
||||
let entry = self.encrypted_keys.get(&key_name)
|
||||
.ok_or_else(|| WsError::auth("Key not found"))?;
|
||||
.ok_or_else(|| "Key not found".to_string())?;
|
||||
|
||||
// Decrypt the private key
|
||||
let private_key = self.decrypt_key(&entry.encrypted_key, &password, &entry.salt)?;
|
||||
|
||||
// Validate the decrypted key
|
||||
if !self.validate_private_key(&private_key)? {
|
||||
return Err(WsError::auth("Failed to decrypt key or invalid key format"));
|
||||
return Err("Failed to decrypt key or invalid key format".to_string());
|
||||
}
|
||||
|
||||
self.state = AuthState::Authenticated {
|
||||
@ -187,7 +191,7 @@ impl BrowserAuthManager {
|
||||
/// Remove a registered key
|
||||
pub fn remove_key(&mut self, key_name: &str) -> WsResult<()> {
|
||||
if self.encrypted_keys.remove(key_name).is_none() {
|
||||
return Err(WsError::auth("Key not found"));
|
||||
return Err("Key not found".to_string());
|
||||
}
|
||||
|
||||
// If we're currently authenticated with this key, logout
|
||||
@ -207,10 +211,10 @@ impl BrowserAuthManager {
|
||||
|
||||
// Use getrandom to get cryptographically secure random bytes
|
||||
getrandom(&mut rng_bytes)
|
||||
.map_err(|e| WsError::auth(format!("Failed to generate random bytes: {}", e)))?;
|
||||
.map_err(|e| format!("Failed to generate random bytes: {}", e))?;
|
||||
|
||||
let secret_key = SecretKey::from_bytes(&rng_bytes.into())
|
||||
.map_err(|e| WsError::auth(format!("Failed to create secret key: {}", e)))?;
|
||||
.map_err(|e| format!("Failed to create secret key: {}", e))?;
|
||||
|
||||
Ok(hex::encode(secret_key.to_bytes()))
|
||||
}
|
||||
@ -222,7 +226,7 @@ impl BrowserAuthManager {
|
||||
if let Ok(Some(data)) = storage.get_item(STORAGE_KEY) {
|
||||
if !data.is_empty() {
|
||||
let keys: HashMap<String, EncryptedKeyEntry> = serde_json::from_str(&data)
|
||||
.map_err(|e| WsError::auth(&format!("Failed to parse stored keys: {}", e)))?;
|
||||
.map_err(|e| format!("Failed to parse stored keys: {}", e))?;
|
||||
self.encrypted_keys = keys;
|
||||
}
|
||||
}
|
||||
@ -234,10 +238,10 @@ impl BrowserAuthManager {
|
||||
fn save_keys(&self) -> WsResult<()> {
|
||||
let storage = self.get_local_storage()?;
|
||||
let data = serde_json::to_string(&self.encrypted_keys)
|
||||
.map_err(|e| WsError::auth(&format!("Failed to serialize keys: {}", e)))?;
|
||||
.map_err(|e| format!("Failed to serialize keys: {}", e))?;
|
||||
|
||||
storage.set_item(STORAGE_KEY, &data)
|
||||
.map_err(|_| WsError::auth("Failed to save keys to storage"))?;
|
||||
.map_err(|_| "Failed to save keys to storage".to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -245,11 +249,11 @@ impl BrowserAuthManager {
|
||||
/// Get browser local storage
|
||||
fn get_local_storage(&self) -> WsResult<Storage> {
|
||||
let window = web_sys::window()
|
||||
.ok_or_else(|| WsError::auth("No window object available"))?;
|
||||
.ok_or_else(|| "No window object available".to_string())?;
|
||||
|
||||
window.local_storage()
|
||||
.map_err(|_| WsError::auth("Failed to access local storage"))?
|
||||
.ok_or_else(|| WsError::auth("Local storage not available"))
|
||||
.map_err(|_| "Failed to access local storage".to_string())?
|
||||
.ok_or_else(|| "Local storage not available".to_string())
|
||||
}
|
||||
|
||||
/// Validate private key format (secp256k1)
|
||||
@ -282,9 +286,9 @@ impl BrowserAuthManager {
|
||||
// Simple XOR encryption for now - in production, use proper encryption
|
||||
// This is a placeholder implementation
|
||||
let key_bytes = hex::decode(private_key)
|
||||
.map_err(|_| WsError::auth("Invalid private key hex"))?;
|
||||
.map_err(|_| "Invalid private key hex".to_string())?;
|
||||
let salt_bytes = hex::decode(salt)
|
||||
.map_err(|_| WsError::auth("Invalid salt hex"))?;
|
||||
.map_err(|_| "Invalid salt hex".to_string())?;
|
||||
|
||||
// Create a key from password and salt using a simple hash
|
||||
let mut password_key = Vec::new();
|
||||
@ -303,14 +307,13 @@ impl BrowserAuthManager {
|
||||
|
||||
Ok(hex::encode(encrypted))
|
||||
}
|
||||
|
||||
/// Decrypt a private key using password and salt
|
||||
fn decrypt_key(&self, encrypted_key: &str, password: &str, salt: &str) -> WsResult<String> {
|
||||
// Simple XOR decryption - matches encrypt_key implementation
|
||||
let encrypted_bytes = hex::decode(encrypted_key)
|
||||
.map_err(|_| WsError::auth("Invalid encrypted key hex"))?;
|
||||
.map_err(|_| "Invalid base64 in stored keys".to_string())?;
|
||||
let salt_bytes = hex::decode(salt)
|
||||
.map_err(|_| WsError::auth("Invalid salt hex"))?;
|
||||
.map_err(|_| "Invalid salt hex".to_string())?;
|
||||
|
||||
// Create the same key from password and salt
|
||||
let mut password_key = Vec::new();
|
@ -1,7 +1,8 @@
|
||||
//! Error types for the WebSocket connection manager
|
||||
|
||||
use thiserror::Error;
|
||||
use circle_client_ws::CircleWsClientError;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use hero_websocket_client::CircleWsClientError;
|
||||
|
||||
/// Result type alias for WebSocket operations
|
||||
pub type WsResult<T> = Result<T, WsError>;
|
||||
@ -10,6 +11,7 @@ pub type WsResult<T> = Result<T, WsError>;
|
||||
#[derive(Error, Debug)]
|
||||
pub enum WsError {
|
||||
/// WebSocket client error from the underlying circle_client_ws library
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("WebSocket client error: {0}")]
|
||||
Client(#[from] CircleWsClientError),
|
||||
|
1775
components/src/file_browser.rs
Normal file
1775
components/src/file_browser.rs
Normal file
File diff suppressed because it is too large
Load Diff
50
components/src/lib.rs
Normal file
50
components/src/lib.rs
Normal file
@ -0,0 +1,50 @@
|
||||
//! Components Library
|
||||
//!
|
||||
//! This library provides reusable UI components for building web applications
|
||||
//! with Rust, Yew, and WebAssembly.
|
||||
|
||||
// Core UI components
|
||||
pub mod file_browser;
|
||||
pub mod markdown_editor;
|
||||
pub mod text_editor;
|
||||
pub mod toast;
|
||||
|
||||
// Auth and utilities (moved from parent)
|
||||
pub mod auth;
|
||||
pub mod browser_auth;
|
||||
pub mod error;
|
||||
pub mod ws_manager;
|
||||
|
||||
// Re-export components for easy access
|
||||
pub use file_browser::FileBrowser;
|
||||
pub use markdown_editor::MarkdownEditor;
|
||||
pub use text_editor::TextEditor;
|
||||
pub use toast::Toast;
|
||||
|
||||
// Re-export utilities
|
||||
pub use auth::*;
|
||||
pub use browser_auth::*;
|
||||
pub use error::*;
|
||||
pub use ws_manager::*;
|
||||
|
||||
// Re-export hero_websocket_client types that users might need
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use hero_websocket_client::{
|
||||
CircleWsClient, CircleWsClientBuilder, CircleWsClientError, methods::PlayResultClient
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use yew::Callback;
|
||||
|
||||
/// Version information
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Prelude module for convenient imports
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
FileBrowser, MarkdownEditor, TextEditor, Toast,
|
||||
BrowserAuth, WsManager, WsManagerBuilder, WsManagerError, AuthConfig
|
||||
};
|
||||
|
||||
pub use crate::VERSION;
|
||||
}
|
301
components/src/markdown_editor.rs
Normal file
301
components/src/markdown_editor.rs
Normal file
@ -0,0 +1,301 @@
|
||||
use yew::prelude::*;
|
||||
use web_sys::{HtmlTextAreaElement, window};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
use gloo_net::http::Request;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct MarkdownEditorProps {
|
||||
pub file_path: String,
|
||||
pub base_endpoint: String,
|
||||
}
|
||||
|
||||
pub struct MarkdownEditor {
|
||||
content: String,
|
||||
rendered_html: String,
|
||||
loading: bool,
|
||||
error: Option<String>,
|
||||
textarea_ref: NodeRef,
|
||||
}
|
||||
|
||||
pub enum MarkdownEditorMsg {
|
||||
LoadFile,
|
||||
FileLoaded(String),
|
||||
FileLoadError(String),
|
||||
ContentChanged(String),
|
||||
SaveFile,
|
||||
FileSaved,
|
||||
FileSaveError(String),
|
||||
}
|
||||
|
||||
impl Component for MarkdownEditor {
|
||||
type Message = MarkdownEditorMsg;
|
||||
type Properties = MarkdownEditorProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let editor = Self {
|
||||
content: String::new(),
|
||||
rendered_html: String::new(),
|
||||
loading: true,
|
||||
error: None,
|
||||
textarea_ref: NodeRef::default(),
|
||||
};
|
||||
|
||||
// Load file content on creation
|
||||
ctx.link().send_message(MarkdownEditorMsg::LoadFile);
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
MarkdownEditorMsg::LoadFile => {
|
||||
let file_url = format!("{}/download/{}", ctx.props().base_endpoint, ctx.props().file_path);
|
||||
let link = ctx.link().clone();
|
||||
|
||||
spawn_local(async move {
|
||||
match Request::get(&file_url).send().await {
|
||||
Ok(response) => {
|
||||
match response.text().await {
|
||||
Ok(content) => {
|
||||
link.send_message(MarkdownEditorMsg::FileLoaded(content));
|
||||
}
|
||||
Err(e) => {
|
||||
link.send_message(MarkdownEditorMsg::FileLoadError(format!("Failed to read file: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
link.send_message(MarkdownEditorMsg::FileLoadError(format!("Failed to load file: {}", e)));
|
||||
}
|
||||
}
|
||||
});
|
||||
false
|
||||
}
|
||||
|
||||
MarkdownEditorMsg::FileLoaded(content) => {
|
||||
self.content = content.clone();
|
||||
self.rendered_html = self.render_markdown(&content);
|
||||
self.loading = false;
|
||||
self.error = None;
|
||||
true
|
||||
}
|
||||
|
||||
MarkdownEditorMsg::FileLoadError(error) => {
|
||||
self.loading = false;
|
||||
self.error = Some(error);
|
||||
true
|
||||
}
|
||||
|
||||
MarkdownEditorMsg::ContentChanged(content) => {
|
||||
self.content = content.clone();
|
||||
self.rendered_html = self.render_markdown(&content);
|
||||
true
|
||||
}
|
||||
|
||||
MarkdownEditorMsg::SaveFile => {
|
||||
let save_url = format!("{}/upload/{}", ctx.props().base_endpoint, ctx.props().file_path);
|
||||
let content = self.content.clone();
|
||||
let link = ctx.link().clone();
|
||||
|
||||
spawn_local(async move {
|
||||
let form_data = web_sys::FormData::new().unwrap();
|
||||
let blob = web_sys::Blob::new_with_str_sequence(&js_sys::Array::of1(&content.into())).unwrap();
|
||||
form_data.append_with_blob("file", &blob).unwrap();
|
||||
|
||||
let mut request_init = web_sys::RequestInit::new();
|
||||
request_init.set_method("POST");
|
||||
let js_value: wasm_bindgen::JsValue = form_data.into();
|
||||
request_init.set_body(&js_value);
|
||||
|
||||
let request = web_sys::Request::new_with_str_and_init(
|
||||
&save_url,
|
||||
&request_init
|
||||
).unwrap();
|
||||
|
||||
match JsFuture::from(window().unwrap().fetch_with_request(&request)).await {
|
||||
Ok(_) => {
|
||||
link.send_message(MarkdownEditorMsg::FileSaved);
|
||||
}
|
||||
Err(e) => {
|
||||
link.send_message(MarkdownEditorMsg::FileSaveError(format!("Failed to save file: {:?}", e)));
|
||||
}
|
||||
}
|
||||
});
|
||||
false
|
||||
}
|
||||
|
||||
MarkdownEditorMsg::FileSaved => {
|
||||
// Show success message or update UI
|
||||
false
|
||||
}
|
||||
|
||||
MarkdownEditorMsg::FileSaveError(error) => {
|
||||
self.error = Some(error);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let filename = ctx.props().file_path.split('/').last().unwrap_or(&ctx.props().file_path);
|
||||
|
||||
if self.loading {
|
||||
return html! {
|
||||
<div class="d-flex justify-content-center align-items-center" style="height: 100vh;">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">{"Loading..."}</span>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(error) = &self.error {
|
||||
return html! {
|
||||
<div class="alert alert-danger m-3" role="alert">
|
||||
<h4 class="alert-heading">{"Error"}</h4>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
};
|
||||
}
|
||||
|
||||
let oninput = ctx.link().callback(|e: InputEvent| {
|
||||
let target = e.target_dyn_into::<HtmlTextAreaElement>().unwrap();
|
||||
MarkdownEditorMsg::ContentChanged(target.value())
|
||||
});
|
||||
|
||||
let onsave = ctx.link().callback(|_| MarkdownEditorMsg::SaveFile);
|
||||
|
||||
html! {
|
||||
<div class="container-fluid h-100">
|
||||
// Header with filename and save button
|
||||
<div class="row bg-light border-bottom p-2">
|
||||
<div class="col">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-file-earmark-text me-2"></i>
|
||||
{filename}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
onclick={onsave}
|
||||
>
|
||||
<i class="bi bi-save me-1"></i>
|
||||
{"Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Split-screen editor
|
||||
<div class="row h-100">
|
||||
// Left pane: Text editor
|
||||
<div class="col-6 p-0">
|
||||
<div class="h-100 d-flex flex-column">
|
||||
<div class="bg-secondary text-white p-2 small">
|
||||
{"Markdown Editor"}
|
||||
</div>
|
||||
<textarea
|
||||
ref={self.textarea_ref.clone()}
|
||||
class="form-control border-0 flex-grow-1"
|
||||
style="resize: none; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 14px;"
|
||||
value={self.content.clone()}
|
||||
oninput={oninput}
|
||||
placeholder="Enter your markdown content here..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Right pane: Live preview
|
||||
<div class="col-6 p-0 border-start">
|
||||
<div class="h-100 d-flex flex-column">
|
||||
<div class="bg-info text-white p-2 small">
|
||||
{"Live Preview"}
|
||||
</div>
|
||||
<div
|
||||
class="flex-grow-1 p-3 overflow-auto"
|
||||
style="background-color: #fff;"
|
||||
>
|
||||
<div class="markdown-content">
|
||||
{Html::from_html_unchecked(AttrValue::from(self.rendered_html.clone()))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MarkdownEditor {
|
||||
fn render_markdown(&self, content: &str) -> String {
|
||||
// For now, use a simple JavaScript-based markdown renderer
|
||||
// In a real implementation, you might want to use a Rust markdown parser
|
||||
let window = window().unwrap();
|
||||
let marked_fn = js_sys::Reflect::get(&window, &"marked".into()).unwrap();
|
||||
|
||||
if marked_fn.is_function() {
|
||||
let marked_fn = marked_fn.dyn_into::<js_sys::Function>().unwrap();
|
||||
let result = marked_fn.call1(&window, &content.into()).unwrap();
|
||||
result.as_string().unwrap_or_else(|| content.to_string())
|
||||
} else {
|
||||
// Fallback: simple markdown transformations using basic string operations
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
let mut result = String::new();
|
||||
|
||||
for line in lines {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("### ") {
|
||||
result.push_str(&format!("<h3>{}</h3>", &trimmed[4..]));
|
||||
} else if trimmed.starts_with("## ") {
|
||||
result.push_str(&format!("<h2>{}</h2>", &trimmed[3..]));
|
||||
} else if trimmed.starts_with("# ") {
|
||||
result.push_str(&format!("<h1>{}</h1>", &trimmed[2..]));
|
||||
} else if trimmed.is_empty() {
|
||||
result.push_str("<br>");
|
||||
} else {
|
||||
// Basic bold and italic (simple cases)
|
||||
let mut processed = trimmed.to_string();
|
||||
|
||||
// Simple bold replacement (**text**)
|
||||
while let Some(start) = processed.find("**") {
|
||||
if let Some(end) = processed[start + 2..].find("**") {
|
||||
let end = end + start + 2;
|
||||
let bold_text = &processed[start + 2..end];
|
||||
processed = format!(
|
||||
"{}<strong>{}</strong>{}",
|
||||
&processed[..start],
|
||||
bold_text,
|
||||
&processed[end + 2..]
|
||||
);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Simple code replacement (`code`)
|
||||
while let Some(start) = processed.find('`') {
|
||||
if let Some(end) = processed[start + 1..].find('`') {
|
||||
let end = end + start + 1;
|
||||
let code_text = &processed[start + 1..end];
|
||||
processed = format!(
|
||||
"{}<code>{}</code>{}",
|
||||
&processed[..start],
|
||||
code_text,
|
||||
&processed[end + 1..]
|
||||
);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.push_str(&format!("<p>{}</p>", processed));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
10
components/src/mod.rs
Normal file
10
components/src/mod.rs
Normal file
@ -0,0 +1,10 @@
|
||||
pub mod toast;
|
||||
pub mod file_browser;
|
||||
pub mod markdown_editor;
|
||||
pub mod text_editor;
|
||||
|
||||
pub use file_browser::{FileBrowser, FileBrowserConfig};
|
||||
pub use markdown_editor::MarkdownEditor;
|
||||
pub use text_editor::TextEditor;
|
||||
|
||||
pub use toast::*;
|
202
components/src/text_editor.rs
Normal file
202
components/src/text_editor.rs
Normal file
@ -0,0 +1,202 @@
|
||||
use yew::prelude::*;
|
||||
use web_sys::{window, HtmlTextAreaElement};
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
use gloo_net::http::Request;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct TextEditorProps {
|
||||
pub file_path: String,
|
||||
pub base_endpoint: String,
|
||||
}
|
||||
|
||||
pub struct TextEditor {
|
||||
content: String,
|
||||
loading: bool,
|
||||
error: Option<String>,
|
||||
textarea_ref: NodeRef,
|
||||
}
|
||||
|
||||
pub enum TextEditorMsg {
|
||||
LoadFile,
|
||||
FileLoaded(String),
|
||||
FileLoadError(String),
|
||||
ContentChanged(String),
|
||||
SaveFile,
|
||||
FileSaved,
|
||||
FileSaveError(String),
|
||||
}
|
||||
|
||||
impl Component for TextEditor {
|
||||
type Message = TextEditorMsg;
|
||||
type Properties = TextEditorProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let editor = Self {
|
||||
content: String::new(),
|
||||
loading: true,
|
||||
error: None,
|
||||
textarea_ref: NodeRef::default(),
|
||||
};
|
||||
|
||||
// Load file content on creation
|
||||
ctx.link().send_message(TextEditorMsg::LoadFile);
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
TextEditorMsg::LoadFile => {
|
||||
let file_url = format!("{}/download/{}", ctx.props().base_endpoint, ctx.props().file_path);
|
||||
let link = ctx.link().clone();
|
||||
|
||||
spawn_local(async move {
|
||||
match Request::get(&file_url).send().await {
|
||||
Ok(response) => {
|
||||
match response.text().await {
|
||||
Ok(content) => {
|
||||
link.send_message(TextEditorMsg::FileLoaded(content));
|
||||
}
|
||||
Err(e) => {
|
||||
link.send_message(TextEditorMsg::FileLoadError(format!("Failed to read file: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
link.send_message(TextEditorMsg::FileLoadError(format!("Failed to load file: {}", e)));
|
||||
}
|
||||
}
|
||||
});
|
||||
false
|
||||
}
|
||||
|
||||
TextEditorMsg::FileLoaded(content) => {
|
||||
self.content = content;
|
||||
self.loading = false;
|
||||
self.error = None;
|
||||
true
|
||||
}
|
||||
|
||||
TextEditorMsg::FileLoadError(error) => {
|
||||
self.loading = false;
|
||||
self.error = Some(error);
|
||||
true
|
||||
}
|
||||
|
||||
TextEditorMsg::ContentChanged(content) => {
|
||||
self.content = content;
|
||||
true
|
||||
}
|
||||
|
||||
TextEditorMsg::SaveFile => {
|
||||
let save_url = format!("{}/upload/{}", ctx.props().base_endpoint, ctx.props().file_path);
|
||||
let content = self.content.clone();
|
||||
let link = ctx.link().clone();
|
||||
|
||||
spawn_local(async move {
|
||||
let form_data = web_sys::FormData::new().unwrap();
|
||||
let blob = web_sys::Blob::new_with_str_sequence(&js_sys::Array::of1(&content.into())).unwrap();
|
||||
form_data.append_with_blob("file", &blob).unwrap();
|
||||
|
||||
let mut request_init = web_sys::RequestInit::new();
|
||||
request_init.set_method("POST");
|
||||
let js_value: wasm_bindgen::JsValue = form_data.into();
|
||||
request_init.set_body(&js_value);
|
||||
|
||||
let request = web_sys::Request::new_with_str_and_init(
|
||||
&save_url,
|
||||
&request_init
|
||||
).unwrap();
|
||||
|
||||
match JsFuture::from(window().unwrap().fetch_with_request(&request)).await {
|
||||
Ok(_) => {
|
||||
link.send_message(TextEditorMsg::FileSaved);
|
||||
}
|
||||
Err(e) => {
|
||||
link.send_message(TextEditorMsg::FileSaveError(format!("Failed to save file: {:?}", e)));
|
||||
}
|
||||
}
|
||||
});
|
||||
false
|
||||
}
|
||||
|
||||
TextEditorMsg::FileSaved => {
|
||||
// Show success message or update UI
|
||||
false
|
||||
}
|
||||
|
||||
TextEditorMsg::FileSaveError(error) => {
|
||||
self.error = Some(error);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let filename = ctx.props().file_path.split('/').last().unwrap_or(&ctx.props().file_path);
|
||||
|
||||
if self.loading {
|
||||
return html! {
|
||||
<div class="d-flex justify-content-center align-items-center" style="height: 100vh;">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">{"Loading..."}</span>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(error) = &self.error {
|
||||
return html! {
|
||||
<div class="alert alert-danger m-3" role="alert">
|
||||
<h4 class="alert-heading">{"Error"}</h4>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
};
|
||||
}
|
||||
|
||||
let oninput = ctx.link().callback(|e: InputEvent| {
|
||||
let target = e.target_dyn_into::<HtmlTextAreaElement>().unwrap();
|
||||
TextEditorMsg::ContentChanged(target.value())
|
||||
});
|
||||
|
||||
let onsave = ctx.link().callback(|_| TextEditorMsg::SaveFile);
|
||||
|
||||
html! {
|
||||
<div class="container-fluid h-100">
|
||||
// Header with filename and save button
|
||||
<div class="row bg-light border-bottom p-2">
|
||||
<div class="col">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-file-earmark-text me-2"></i>
|
||||
{filename}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
onclick={onsave}
|
||||
>
|
||||
<i class="bi bi-save me-1"></i>
|
||||
{"Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Full-screen text editor
|
||||
<div class="row h-100">
|
||||
<div class="col p-0">
|
||||
<textarea
|
||||
ref={self.textarea_ref.clone()}
|
||||
class="form-control border-0 h-100"
|
||||
style="resize: none; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 14px;"
|
||||
value={self.content.clone()}
|
||||
oninput={oninput}
|
||||
placeholder="Enter your text content here..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
@ -194,7 +194,7 @@ pub fn use_toast() -> (Vec<Toast>, Callback<Toast>, Callback<String>) {
|
||||
let manager = use_mut_ref(|| ToastManager::new());
|
||||
let toasts = use_state(|| Vec::<Toast>::new());
|
||||
|
||||
let update_toasts = {
|
||||
let _update_toasts = {
|
||||
let manager = manager.clone();
|
||||
let toasts = toasts.clone();
|
||||
move || {
|
@ -1,22 +1,79 @@
|
||||
//! WebSocket Manager
|
||||
//!
|
||||
//! A lightweight manager for multiple self-managing WebSocket connections.
|
||||
//! Since `CircleWsClient` handles connection lifecycle, authentication, and keep-alive
|
||||
//! internally, this manager focuses on simple orchestration and script execution.
|
||||
//! Handles connection lifecycle, authentication, and keep-alive for multiple WebSocket URLs.
|
||||
//! Provides access to live authorized clients for external use.
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! The `WsManager` is designed to:
|
||||
//! - Connect to multiple WebSocket servers simultaneously
|
||||
//! - Handle authentication using private keys
|
||||
//! - Maintain persistent connections with automatic keep-alive
|
||||
//! - Provide safe access to connected clients for script execution and other operations
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```rust
|
||||
//! use framework::WsManager;
|
||||
//!
|
||||
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! // Create a manager with multiple servers
|
||||
//! let manager = WsManager::builder()
|
||||
//! .private_key("your_private_key_hex".to_string())
|
||||
//! .add_server_url("ws://localhost:3030".to_string())
|
||||
//! .add_server_url("ws://localhost:3031".to_string())
|
||||
//! .build();
|
||||
//!
|
||||
//! // Connect to all servers
|
||||
//! manager.connect().await?;
|
||||
//!
|
||||
//! // Execute operations on a specific server
|
||||
//! if let Some(result) = manager.with_client("ws://localhost:3030", |client| {
|
||||
//! client.play("console.log('Hello World!');".to_string())
|
||||
//! }).await {
|
||||
//! match result {
|
||||
//! Ok(output) => println!("Script output: {}", output),
|
||||
//! Err(e) => eprintln!("Script error: {}", e),
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // Work with all connected clients
|
||||
//! manager.with_all_clients(|clients| {
|
||||
//! for (url, client) in clients {
|
||||
//! if client.is_connected() {
|
||||
//! println!("Connected to: {}", url);
|
||||
//! }
|
||||
//! }
|
||||
//! });
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! The manager uses a builder pattern for configuration and maintains clients in a `RefCell<HashMap>`
|
||||
//! for interior mutability. Since `CircleWsClient` doesn't implement `Clone`, access is provided
|
||||
//! through closure-based methods that ensure safe borrowing.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use log::{error, info};
|
||||
use circle_client_ws::{CircleWsClient, CircleWsClientBuilder, CircleWsClientError, PlayResultClient};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use hero_websocket_client::{CircleWsClient, CircleWsClientBuilder, CircleWsClientError};
|
||||
|
||||
/// Builder for creating a WebSocket manager
|
||||
///
|
||||
/// Use this to configure multiple WebSocket URLs and optional authentication
|
||||
/// before building the final `WsManager` instance.
|
||||
pub struct WsManagerBuilder {
|
||||
private_key: Option<String>,
|
||||
server_urls: Vec<String>,
|
||||
}
|
||||
|
||||
impl WsManagerBuilder {
|
||||
/// Create a new builder instance
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
private_key: None,
|
||||
@ -25,18 +82,27 @@ impl WsManagerBuilder {
|
||||
}
|
||||
|
||||
/// Set private key for authentication
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `private_key` - Hex-encoded private key for signing authentication challenges
|
||||
pub fn private_key(mut self, private_key: String) -> Self {
|
||||
self.private_key = Some(private_key);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a server URL
|
||||
/// Add a server URL to connect to
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `url` - WebSocket URL (e.g., "ws://localhost:3030")
|
||||
pub fn add_server_url(mut self, url: String) -> Self {
|
||||
self.server_urls.push(url);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the manager and create clients for all URLs
|
||||
///
|
||||
/// Creates `CircleWsClient` instances for each configured URL.
|
||||
/// Clients are not connected until `connect()` is called.
|
||||
pub fn build(self) -> WsManager {
|
||||
let mut clients = HashMap::new();
|
||||
|
||||
@ -126,26 +192,70 @@ impl WsManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute script on a specific server
|
||||
pub async fn execute_script(&self, url: &str, script: String) -> Result<PlayResultClient, CircleWsClientError> {
|
||||
|
||||
|
||||
/// Execute a closure with access to a connected client
|
||||
///
|
||||
/// This is the preferred way to access clients since CircleWsClient doesn't implement Clone.
|
||||
/// The closure is only executed if the client exists and is connected.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `url` - The WebSocket URL of the target server
|
||||
/// * `f` - Closure that receives a reference to the connected client
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Some(R)` - Result of the closure if client is connected
|
||||
/// * `None` - If client doesn't exist or is not connected
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use framework::WsManager;
|
||||
/// # async fn example(manager: &WsManager) {
|
||||
/// let result = manager.with_client("ws://localhost:3030", |client| {
|
||||
/// client.play("console.log('Hello!');".to_string())
|
||||
/// }).await;
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn with_client<F, R>(&self, url: &str, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&CircleWsClient) -> R,
|
||||
{
|
||||
let clients = self.clients.borrow();
|
||||
match clients.get(url) {
|
||||
Some(client) => client.play(script).await,
|
||||
None => Err(CircleWsClientError::NotConnected),
|
||||
if let Some(client) = clients.get(url) {
|
||||
if client.is_connected() {
|
||||
return Some(f(client));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Execute script on all connected servers
|
||||
pub async fn execute_script_on_all(&self, script: String) -> HashMap<String, Result<PlayResultClient, CircleWsClientError>> {
|
||||
let mut results = HashMap::new();
|
||||
let urls: Vec<String> = self.clients.borrow().keys().cloned().collect();
|
||||
|
||||
for url in urls {
|
||||
let result = self.execute_script(&url, script.clone()).await;
|
||||
results.insert(url, result);
|
||||
}
|
||||
|
||||
results
|
||||
/// Execute a closure with access to all clients
|
||||
///
|
||||
/// Provides access to the entire HashMap of clients, including both connected
|
||||
/// and disconnected ones. Use `client.is_connected()` to check status.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `f` - Closure that receives a reference to the clients HashMap
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use framework::WsManager;
|
||||
/// # fn example(manager: &WsManager) {
|
||||
/// manager.with_all_clients(|clients| {
|
||||
/// for (url, client) in clients {
|
||||
/// if client.is_connected() {
|
||||
/// println!("Connected to: {}", url);
|
||||
/// }
|
||||
/// }
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn with_all_clients<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&HashMap<String, CircleWsClient>) -> R,
|
||||
{
|
||||
let clients = self.clients.borrow();
|
||||
f(&*clients)
|
||||
}
|
||||
|
||||
/// Get connected server URLs
|
135
examples/README.md
Normal file
135
examples/README.md
Normal file
@ -0,0 +1,135 @@
|
||||
# Framework Examples
|
||||
|
||||
This directory contains examples demonstrating how to use the Hero Framework components.
|
||||
|
||||
## WebSocket Manager Demo
|
||||
|
||||
The `ws_manager_demo.rs` example demonstrates the complete usage of the `WsManager` for connecting to multiple WebSocket servers, handling authentication, and executing scripts.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Start Hero WebSocket Server(s)**:
|
||||
```bash
|
||||
# Terminal 1 - Start first server
|
||||
cd ../hero/interfaces/websocket/server
|
||||
cargo run --example circle_auth_demo -- --port 3030
|
||||
|
||||
# Terminal 2 - Start second server (optional)
|
||||
cargo run --example circle_auth_demo -- --port 3031
|
||||
```
|
||||
|
||||
2. **Run the Example**:
|
||||
```bash
|
||||
# From the framework directory
|
||||
cargo run --example ws_manager_demo
|
||||
```
|
||||
|
||||
### What the Demo Shows
|
||||
|
||||
The example demonstrates:
|
||||
|
||||
- **Multi-server Connection**: Connecting to multiple WebSocket servers simultaneously
|
||||
- **Authentication**: Using private keys for server authentication
|
||||
- **Script Execution**: Running JavaScript/Rhai scripts on connected servers
|
||||
- **Connection Management**: Connecting, disconnecting, and reconnecting to specific servers
|
||||
- **Status Monitoring**: Checking connection and authentication status
|
||||
- **Runtime Management**: Adding new connections dynamically
|
||||
- **Error Handling**: Graceful handling of connection and execution errors
|
||||
|
||||
### Key Code Patterns
|
||||
|
||||
#### Creating and Connecting
|
||||
```rust
|
||||
let manager = WsManager::builder()
|
||||
.private_key("your_private_key_hex".to_string())
|
||||
.add_server_url("ws://localhost:3030".to_string())
|
||||
.add_server_url("ws://localhost:3031".to_string())
|
||||
.build();
|
||||
|
||||
manager.connect().await?;
|
||||
```
|
||||
|
||||
#### Executing Scripts on Specific Servers
|
||||
```rust
|
||||
if let Some(result) = manager.with_client("ws://localhost:3030", |client| {
|
||||
client.play("console.log('Hello World!');".to_string())
|
||||
}).await {
|
||||
match result {
|
||||
Ok(output) => println!("Script output: {}", output),
|
||||
Err(e) => eprintln!("Script error: {}", e),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Working with All Connected Clients
|
||||
```rust
|
||||
manager.with_all_clients(|clients| {
|
||||
for (url, client) in clients {
|
||||
if client.is_connected() {
|
||||
// Use the client for operations
|
||||
println!("Connected to: {}", url);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Connection Management
|
||||
```rust
|
||||
// Disconnect from specific server
|
||||
manager.disconnect_from_server("ws://localhost:3031").await?;
|
||||
|
||||
// Reconnect
|
||||
manager.connect_to_server("ws://localhost:3031").await?;
|
||||
|
||||
// Add new connection at runtime
|
||||
manager.add_connection(
|
||||
"ws://localhost:3032".to_string(),
|
||||
Some(private_key.to_string())
|
||||
);
|
||||
```
|
||||
|
||||
### Expected Output
|
||||
|
||||
When running the demo with servers available, you should see output like:
|
||||
|
||||
```
|
||||
🚀 Starting WebSocket Manager Demo
|
||||
📡 Connecting to WebSocket servers...
|
||||
✅ Successfully initiated connections
|
||||
🔍 Checking connection status...
|
||||
ws://localhost:3030 -> Connected
|
||||
ws://localhost:3031 -> Connected
|
||||
🎯 Executing script on specific server...
|
||||
📤 Sending script to ws://localhost:3030
|
||||
✅ Script output: Script executed successfully
|
||||
🌐 Executing operations on all connected clients...
|
||||
📤 Pinging ws://localhost:3030
|
||||
📤 Pinging ws://localhost:3031
|
||||
✅ ws://localhost:3030 responded: pong from 2024-01-15T10:30:45.123Z
|
||||
✅ ws://localhost:3031 responded: pong from 2024-01-15T10:30:45.124Z
|
||||
🎉 WebSocket Manager Demo completed!
|
||||
```
|
||||
|
||||
## Website Example
|
||||
|
||||
The `website` directory contains a complete Yew-based web application demonstrating the framework's browser capabilities, including:
|
||||
|
||||
- WebSocket client management in WASM
|
||||
- Browser-based authentication
|
||||
- Interactive script execution
|
||||
- Real-time connection monitoring
|
||||
|
||||
See `website/README.md` for details on running the web application.
|
||||
|
||||
## Tips for Development
|
||||
|
||||
1. **Logging**: Set `RUST_LOG=debug` for detailed connection logs
|
||||
2. **Testing**: Use multiple terminal windows to run servers on different ports
|
||||
3. **Authentication**: In production, load private keys from secure storage, not hardcoded strings
|
||||
4. **Error Handling**: The examples show basic error handling patterns - extend as needed for production use
|
||||
|
||||
## Common Issues
|
||||
|
||||
- **Connection Refused**: Make sure the WebSocket servers are running before starting the demo
|
||||
- **Authentication Failures**: Verify that the private key format is correct (64-character hex string)
|
||||
- **Port Conflicts**: Use different ports if the default ones (3030, 3031) are already in use
|
2995
examples/file_browser_demo/Cargo.lock
generated
Normal file
2995
examples/file_browser_demo/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
examples/file_browser_demo/Cargo.toml
Normal file
27
examples/file_browser_demo/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "file_browser_demo"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# Make this package independent from the parent workspace
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
framework = { path = "../.." }
|
||||
yew = { version = "0.21", features = ["csr"] }
|
||||
yew-router = "0.18"
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = "0.3"
|
||||
js-sys = "0.3"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
gloo = "0.11"
|
||||
console_error_panic_hook = "0.1"
|
||||
wee_alloc = "0.4"
|
||||
|
||||
[dependencies.getrandom]
|
||||
version = "0.2"
|
||||
features = ["js"]
|
||||
|
||||
[[bin]]
|
||||
name = "file_browser_demo"
|
||||
path = "src/main.rs"
|
273
examples/file_browser_demo/README.md
Normal file
273
examples/file_browser_demo/README.md
Normal file
@ -0,0 +1,273 @@
|
||||
# File Browser Demo
|
||||
|
||||
A comprehensive file browser component built with Yew (Rust) and compiled to WebAssembly, featuring Uppy.js integration for resumable file uploads via the TUS protocol.
|
||||
|
||||
## Features
|
||||
|
||||
- 📁 **File System Browser**: Navigate directories, view files with metadata
|
||||
- ⬆️ **Resumable Uploads**: TUS protocol support via Uppy.js for reliable file uploads
|
||||
- ⬇️ **File Downloads**: Direct download with progress tracking
|
||||
- 🗂️ **Directory Management**: Create and delete directories
|
||||
- 🗑️ **File Management**: Delete files with confirmation
|
||||
- 📊 **Progress Tracking**: Real-time upload progress with visual indicators
|
||||
- 🎨 **Modern UI**: Bootstrap-styled responsive interface
|
||||
- 🚀 **WebAssembly**: High-performance Rust code compiled to WASM
|
||||
|
||||
## Architecture
|
||||
|
||||
### Component Structure
|
||||
|
||||
```
|
||||
FileBrowser
|
||||
├── FileBrowserConfig (Properties)
|
||||
├── FileBrowserMsg (Messages)
|
||||
├── API Functions (HTTP calls to backend)
|
||||
└── Uppy.js Integration (JavaScript interop)
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **FileBrowser**: Main Yew component with file listing, navigation, and upload UI
|
||||
2. **FileBrowserConfig**: Configuration struct for customizing the widget
|
||||
3. **API Layer**: Async functions for backend communication using web_sys::fetch
|
||||
4. **Uppy Integration**: JavaScript interop for TUS resumable uploads
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The `FileBrowserConfig` struct allows extensive customization:
|
||||
|
||||
```rust
|
||||
FileBrowserConfig {
|
||||
base_endpoint: "/files".to_string(), // Backend API endpoint
|
||||
max_file_size: 100 * 1024 * 1024, // Max file size (100MB)
|
||||
chunk_size: 1024 * 1024, // Download chunk size (1MB)
|
||||
initial_path: "".to_string(), // Starting directory
|
||||
show_upload: true, // Enable upload functionality
|
||||
show_download: true, // Enable download functionality
|
||||
show_delete: true, // Enable delete functionality
|
||||
show_create_dir: true, // Enable directory creation
|
||||
css_classes: "container-fluid".to_string(), // Custom CSS classes
|
||||
theme: "light".to_string(), // Uppy theme (light/dark)
|
||||
}
|
||||
```
|
||||
|
||||
## Backend Compatibility
|
||||
|
||||
The file browser component is designed to work with the Python Flask backend from `src/files.py`. It expects the following API endpoints:
|
||||
|
||||
- `GET /files/list/{path}` - List directory contents
|
||||
- `POST /files/upload` - TUS resumable upload endpoint
|
||||
- `GET /files/download/{path}` - Download files
|
||||
- `POST /files/dirs/{path}` - Create directories
|
||||
- `DELETE /files/delete/{path}` - Delete files/directories
|
||||
|
||||
### Mock Server
|
||||
|
||||
For testing and development, this demo includes a Rust-based mock server that implements the same API as the Python backend:
|
||||
|
||||
**Location:** `mock-server/`
|
||||
|
||||
**Features:**
|
||||
- Full API compatibility with `src/files.py`
|
||||
- Sample files and directories for testing
|
||||
- CORS enabled for frontend development
|
||||
- Lightweight and fast
|
||||
- No external dependencies beyond Rust
|
||||
|
||||
**Manual Usage:**
|
||||
```bash
|
||||
# Start just the mock server
|
||||
./run-mock-server.sh
|
||||
|
||||
# Or run manually
|
||||
cd mock-server
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Rust (latest stable version)
|
||||
- `trunk` for building and serving WASM applications:
|
||||
```bash
|
||||
cargo install trunk
|
||||
```
|
||||
- `wasm32-unknown-unknown` target:
|
||||
```bash
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
### Easy Demo Launch
|
||||
|
||||
The quickest way to see the file browser in action:
|
||||
|
||||
```bash
|
||||
./run-demo.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
1. Build and start the Rust mock server on `http://localhost:3001`
|
||||
2. Build and serve the WASM demo on `http://localhost:8080`
|
||||
3. Automatically open your browser to the demo
|
||||
4. Handle cleanup when you press Ctrl+C
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Rust and Trunk**:
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
cargo install trunk
|
||||
```
|
||||
|
||||
2. **Backend Server**: The Python Flask backend from the knowledgecenter project
|
||||
|
||||
## Building and Running
|
||||
|
||||
### 1. Build the WASM Application
|
||||
|
||||
```bash
|
||||
# From the file_browser_demo directory
|
||||
./build.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
- Build the Rust code to WebAssembly using Trunk
|
||||
- Generate optimized WASM and JavaScript files
|
||||
- Output files to the `dist/` directory
|
||||
|
||||
### 2. Start the Backend Server
|
||||
|
||||
```bash
|
||||
# From the knowledgecenter directory
|
||||
cd /path/to/knowledgecenter
|
||||
python -m flask run
|
||||
```
|
||||
|
||||
Make sure CORS is configured to allow requests from your frontend origin.
|
||||
|
||||
### 3. Serve the Frontend
|
||||
|
||||
**Development Mode:**
|
||||
```bash
|
||||
# From the file_browser_demo directory
|
||||
trunk serve
|
||||
```
|
||||
|
||||
**Production Mode:**
|
||||
```bash
|
||||
# From the file_browser_demo directory
|
||||
./serve.sh
|
||||
```
|
||||
|
||||
### 4. Open in Browser
|
||||
|
||||
Trunk will automatically open `http://127.0.0.1:8080` in your web browser.
|
||||
|
||||
## Usage as a Widget
|
||||
|
||||
The file browser can be used as a reusable widget in other Yew applications:
|
||||
|
||||
```rust
|
||||
use framework::components::{FileBrowser, FileBrowserConfig};
|
||||
|
||||
#[function_component(MyApp)]
|
||||
fn my_app() -> Html {
|
||||
let config = FileBrowserConfig {
|
||||
base_endpoint: "/api/files".to_string(),
|
||||
initial_path: "documents".to_string(),
|
||||
theme: "dark".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="my-app">
|
||||
<FileBrowser ..config />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Styling
|
||||
|
||||
The component uses Bootstrap classes and can be customized via:
|
||||
|
||||
1. **CSS Classes**: Pass custom classes via `css_classes` in config
|
||||
2. **Theme**: Set Uppy theme to "light" or "dark"
|
||||
3. **Custom CSS**: Override the default styles in your application
|
||||
|
||||
### Functionality
|
||||
|
||||
Enable/disable features via configuration:
|
||||
|
||||
```rust
|
||||
FileBrowserConfig {
|
||||
show_upload: false, // Hide upload functionality
|
||||
show_delete: false, // Hide delete buttons
|
||||
show_create_dir: false, // Hide directory creation
|
||||
// ... other options
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
file_browser_demo/
|
||||
├── Cargo.toml # Rust dependencies
|
||||
├── build.sh # Build script
|
||||
├── index.html # HTML template with Uppy.js
|
||||
├── src/
|
||||
│ └── main.rs # Main Yew application
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
### Key Dependencies
|
||||
|
||||
- **yew**: Rust web framework
|
||||
- **wasm-bindgen**: Rust/JavaScript interop
|
||||
- **web-sys**: Web API bindings
|
||||
- **serde**: Serialization for API communication
|
||||
- **js-sys**: JavaScript value manipulation
|
||||
|
||||
### JavaScript Dependencies
|
||||
|
||||
- **Uppy.js**: File upload library with TUS support
|
||||
- **Bootstrap**: UI framework
|
||||
- **Bootstrap Icons**: Icon set
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### WASM Module Loading Issues
|
||||
|
||||
1. Ensure files are served over HTTP (not file://)
|
||||
2. Check browser console for detailed error messages
|
||||
3. Verify WASM files are generated in `pkg/` directory
|
||||
|
||||
### Upload Issues
|
||||
|
||||
1. Check backend server is running and accessible
|
||||
2. Verify CORS configuration allows your frontend origin
|
||||
3. Ensure TUS endpoints are properly implemented in backend
|
||||
|
||||
### Build Issues
|
||||
|
||||
1. Update Rust toolchain: `rustup update`
|
||||
2. Clear cargo cache: `cargo clean`
|
||||
3. Reinstall wasm-pack if needed
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Chrome/Chromium 80+
|
||||
- Firefox 72+
|
||||
- Safari 13.1+
|
||||
- Edge 80+
|
||||
|
||||
WebAssembly and modern JavaScript features are required.
|
||||
|
||||
## License
|
||||
|
||||
This demo is part of the Hero Framework project. See the main project for licensing information.
|
31
examples/file_browser_demo/Trunk.toml
Normal file
31
examples/file_browser_demo/Trunk.toml
Normal file
@ -0,0 +1,31 @@
|
||||
[build]
|
||||
target = "index.html"
|
||||
dist = "dist"
|
||||
|
||||
[serve]
|
||||
address = "127.0.0.1"
|
||||
port = 8080
|
||||
open = true
|
||||
|
||||
[tools]
|
||||
# Aggressive WASM optimization with wasm-opt
|
||||
wasm-opt = [
|
||||
"-Os", # Optimize for size
|
||||
"--enable-mutable-globals",
|
||||
"--enable-sign-ext",
|
||||
"--enable-nontrapping-float-to-int",
|
||||
"--enable-bulk-memory",
|
||||
"--strip-debug", # Remove debug info
|
||||
"--strip-producers", # Remove producer info
|
||||
"--dce", # Dead code elimination
|
||||
"--vacuum", # Remove unused code
|
||||
"--merge-blocks", # Merge basic blocks
|
||||
"--precompute", # Precompute expressions
|
||||
"--precompute-propagate", # Propagate precomputed values
|
||||
"--remove-unused-names", # Remove unused function names
|
||||
"--simplify-locals", # Simplify local variables
|
||||
"--coalesce-locals", # Coalesce local variables
|
||||
"--reorder-locals", # Reorder locals for better compression
|
||||
"--flatten", # Flatten control flow
|
||||
"--rereloop", # Optimize loops
|
||||
]
|
17
examples/file_browser_demo/build.sh
Executable file
17
examples/file_browser_demo/build.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Building File Browser Demo with Trunk..."
|
||||
|
||||
# Check if trunk is installed
|
||||
if ! command -v trunk &> /dev/null; then
|
||||
echo "Trunk is not installed. Installing..."
|
||||
cargo install trunk
|
||||
fi
|
||||
|
||||
# Build for development
|
||||
echo "Building for development..."
|
||||
trunk build
|
||||
|
||||
echo "Build complete! Files are in the 'dist' directory."
|
||||
echo "To serve locally, run: trunk serve"
|
||||
echo "To build for production, run: trunk build --release"
|
201
examples/file_browser_demo/index.html
Normal file
201
examples/file_browser_demo/index.html
Normal file
@ -0,0 +1,201 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>File Browser Demo - Yew + Uppy.js + TUS</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Uppy CSS -->
|
||||
<link href="https://releases.transloadit.com/uppy/v4.13.3/uppy.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-browser {
|
||||
padding: 1.5rem;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.file-browser-toolbar {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-browser-items {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.file-browser-items .table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.file-browser-items .table th {
|
||||
background-color: #f8f9fa;
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.file-browser-items .table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.file-browser-items .table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.file-browser-upload .card {
|
||||
border: 2px dashed #dee2e6;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.file-browser-upload .card-header {
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||
border-bottom: 1px solid #90caf9;
|
||||
}
|
||||
|
||||
/* Uppy Dashboard Styling */
|
||||
.uppy-Dashboard {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.uppy-Dashboard-inner {
|
||||
border: 2px dashed #007bff !important;
|
||||
border-radius: 0.5rem !important;
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #e6f3ff 100%) !important;
|
||||
}
|
||||
|
||||
.uppy-Dashboard-dropFilesHereHint {
|
||||
color: #007bff !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
/* Progress bars */
|
||||
.progress {
|
||||
height: 0.5rem;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: linear-gradient(90deg, #007bff 0%, #0056b3 100%);
|
||||
}
|
||||
|
||||
/* Breadcrumb styling */
|
||||
.file-browser-breadcrumb .breadcrumb {
|
||||
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.file-browser-breadcrumb .breadcrumb-item a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.file-browser-breadcrumb .breadcrumb-item a:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Modal styling */
|
||||
.modal-content {
|
||||
border: none;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
||||
color: white;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.modal-header .btn-close {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.btn-outline-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.btn-outline-success:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(40, 167, 69, 0.25);
|
||||
}
|
||||
|
||||
.btn-outline-danger:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.spinner-border {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #007bff !important;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #0056b3 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- Trunk will automatically inject the WASM loading script here -->
|
||||
<link data-trunk rel="rust" data-bin="file_browser_demo" />
|
||||
|
||||
<!-- Uppy.js (ES Modules) -->
|
||||
<script type="module">
|
||||
// Import Uppy modules and make them globally available
|
||||
import { Uppy, Dashboard, Tus, GoogleDrive } from "https://releases.transloadit.com/uppy/v4.13.3/uppy.min.mjs";
|
||||
|
||||
// Make Uppy available globally for the Rust/WASM code
|
||||
window.Uppy = Uppy;
|
||||
window.Uppy.Dashboard = Dashboard;
|
||||
window.Uppy.Tus = Tus;
|
||||
window.Uppy.GoogleDrive = GoogleDrive;
|
||||
|
||||
console.log("Uppy.js loaded and available globally");
|
||||
</script>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
1279
examples/file_browser_demo/mock-server/Cargo.lock
generated
Normal file
1279
examples/file_browser_demo/mock-server/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
examples/file_browser_demo/mock-server/Cargo.toml
Normal file
25
examples/file_browser_demo/mock-server/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "file-browser-mock-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
|
||||
[[bin]]
|
||||
name = "mock-server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.7"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tower-http = { version = "0.5", features = ["cors"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
anyhow = "1.0"
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
walkdir = "2.3"
|
||||
base64 = "0.21"
|
@ -0,0 +1,3 @@
|
||||
# File Browser Demo
|
||||
|
||||
This is a sample file for testing the file browser component.
|
@ -0,0 +1 @@
|
||||
Sample notes file content.
|
@ -0,0 +1,3 @@
|
||||
# Sample Report
|
||||
|
||||
This is a sample markdown report.
|
@ -0,0 +1 @@
|
||||
Placeholder for image files.
|
@ -0,0 +1 @@
|
||||
{"name": "sample-project", "version": "1.0.0"}
|
@ -0,0 +1,3 @@
|
||||
# Project 1
|
||||
|
||||
Sample project documentation.
|
@ -0,0 +1 @@
|
||||
This is a sample text file.
|
@ -0,0 +1,3 @@
|
||||
# File Browser Demo
|
||||
|
||||
This is a sample file for testing the file browser component.
|
Binary file not shown.
Binary file not shown.
59
examples/file_browser_demo/mock-server/mock_files/design.md
Normal file
59
examples/file_browser_demo/mock-server/mock_files/design.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines a system design that satisfies the specified requirements for decentralized backend ownership. It describes how to implement core capabilities like isolation, delegation, and open logic control — without introducing tight coupling or central dependencies.
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. **Contextual Execution**
|
||||
- Define a runtime model where each peer context is a named environment.
|
||||
- Execution is scoped to a context, and all operations are resolved within it.
|
||||
|
||||
**Implementation Strategy:**
|
||||
- Use a unified worker engine that can load and execute within a namespaced peer context.
|
||||
- Contexts are mounted via a virtual filesystem abstraction, one directory per peer.
|
||||
|
||||
### 2. **Logical Isolation via Filesystem Namespacing**
|
||||
- Each peer's execution environment is backed by a namespaced root directory.
|
||||
- All storage operations are relative to that root.
|
||||
|
||||
**Advantages:**
|
||||
- Easy enforcement of data boundaries
|
||||
- Works across shared processes
|
||||
|
||||
### 3. **Script-Based Delegated Execution**
|
||||
- Scripts are the unit of cross-peer interaction.
|
||||
- A script includes the `caller` (originating peer), parameters, and logic.
|
||||
|
||||
**Design Feature:**
|
||||
- A script sent to another peer is evaluated with both `caller` and `target` contexts available to the runtime.
|
||||
- Target peer decides whether to accept and how to interpret it.
|
||||
|
||||
### 4. **Policy-Driven Acceptance**
|
||||
- Each context has policies determining:
|
||||
- Which peers may send scripts
|
||||
- Which actions are allowed
|
||||
|
||||
**Example:** Policies written as declarative access control rules, tied to peer IDs, namespaces, or capabilities.
|
||||
|
||||
### 5. **Open, Modifiable Logic**
|
||||
- Use an embedded domain-specific language (e.g. Rhai) that allows:
|
||||
- Peer owners to define and inspect their logic
|
||||
- Script modules to be composed, extended, or overridden
|
||||
|
||||
### 6. **Worker Multiplexing**
|
||||
- Use a single worker binary that can handle one or many peer contexts.
|
||||
- The context is dynamically determined at runtime.
|
||||
|
||||
**Design Note:**
|
||||
- All workers enforce namespacing, even when only one peer is active per process.
|
||||
- Supports both isolated (1 peer per worker) and shared (many peers per worker) deployments.
|
||||
|
||||
## Optional Enhancements
|
||||
|
||||
- Pluggable transport layer (WebSocket, HTTP/2, NATS, etc.)
|
||||
- Pluggable storage backends for namespace-mounting (FS, S3, SQLite, etc.)
|
||||
- Declarative schema binding between DSL and structured data
|
||||
|
||||
This design enables decentralized application runtime control while supporting a scalable and secure execution model.
|
@ -0,0 +1 @@
|
||||
Sample notes file content.
|
@ -0,0 +1,3 @@
|
||||
# Sample Report
|
||||
|
||||
This is a sample markdown report.
|
Binary file not shown.
@ -0,0 +1 @@
|
||||
Placeholder for image files.
|
@ -0,0 +1 @@
|
||||
{"name": "sample-project", "version": "1.0.0"}
|
@ -0,0 +1,59 @@
|
||||
# Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines a system design that satisfies the specified requirements for decentralized backend ownership. It describes how to implement core capabilities like isolation, delegation, and open logic control — without introducing tight coupling or central dependencies.
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. **Contextual Execution**
|
||||
- Define a runtime model where each peer context is a named environment.
|
||||
- Execution is scoped to a context, and all operations are resolved within it.
|
||||
|
||||
**Implementation Strategy:**
|
||||
- Use a unified worker engine that can load and execute within a namespaced peer context.
|
||||
- Contexts are mounted via a virtual filesystem abstraction, one directory per peer.
|
||||
|
||||
### 2. **Logical Isolation via Filesystem Namespacing**
|
||||
- Each peer's execution environment is backed by a namespaced root directory.
|
||||
- All storage operations are relative to that root.
|
||||
|
||||
**Advantages:**
|
||||
- Easy enforcement of data boundaries
|
||||
- Works across shared processes
|
||||
|
||||
### 3. **Script-Based Delegated Execution**
|
||||
- Scripts are the unit of cross-peer interaction.
|
||||
- A script includes the `caller` (originating peer), parameters, and logic.
|
||||
|
||||
**Design Feature:**
|
||||
- A script sent to another peer is evaluated with both `caller` and `target` contexts available to the runtime.
|
||||
- Target peer decides whether to accept and how to interpret it.
|
||||
|
||||
### 4. **Policy-Driven Acceptance**
|
||||
- Each context has policies determining:
|
||||
- Which peers may send scripts
|
||||
- Which actions are allowed
|
||||
|
||||
**Example:** Policies written as declarative access control rules, tied to peer IDs, namespaces, or capabilities.
|
||||
|
||||
### 5. **Open, Modifiable Logic**
|
||||
- Use an embedded domain-specific language (e.g. Rhai) that allows:
|
||||
- Peer owners to define and inspect their logic
|
||||
- Script modules to be composed, extended, or overridden
|
||||
|
||||
### 6. **Worker Multiplexing**
|
||||
- Use a single worker binary that can handle one or many peer contexts.
|
||||
- The context is dynamically determined at runtime.
|
||||
|
||||
**Design Note:**
|
||||
- All workers enforce namespacing, even when only one peer is active per process.
|
||||
- Supports both isolated (1 peer per worker) and shared (many peers per worker) deployments.
|
||||
|
||||
## Optional Enhancements
|
||||
|
||||
- Pluggable transport layer (WebSocket, HTTP/2, NATS, etc.)
|
||||
- Pluggable storage backends for namespace-mounting (FS, S3, SQLite, etc.)
|
||||
- Declarative schema binding between DSL and structured data
|
||||
|
||||
This design enables decentralized application runtime control while supporting a scalable and secure execution model.
|
@ -0,0 +1,3 @@
|
||||
# Project 1
|
||||
|
||||
Sample project documentation.
|
@ -0,0 +1,50 @@
|
||||
# System Requirements Specification
|
||||
|
||||
## Objective
|
||||
|
||||
To define the core requirements for a system that fulfills the goals of decentralized backend ownership — enabling individuals and organizations to control, operate, and interact through their own backend environments without relying on centralized infrastructure.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### 1. **Isolated Execution Contexts**
|
||||
- Each user or peer must operate within a distinct, logically isolated execution context.
|
||||
- Contexts must not be able to interfere with each other's state or runtime.
|
||||
|
||||
### 2. **Cross-Context Communication**
|
||||
- Peers must be able to initiate interactions with other peers.
|
||||
- Communication must include origin metadata (who initiated it), and be authorized by the target context.
|
||||
|
||||
### 3. **Delegated Execution**
|
||||
- A peer must be able to send code or instructions to another peer for execution, under the recipient's policies.
|
||||
- The recipient must treat the execution as contextualized by the caller, but constrained by its own local rules.
|
||||
|
||||
### 4. **Ownership of Logic and Data**
|
||||
- Users must be able to inspect, modify, and extend the logic that governs their backend.
|
||||
- Data storage and access policies must be under the control of the peer.
|
||||
|
||||
### 5. **Composability and Modifiability**
|
||||
- System behavior must be defined by open, composable modules or scripts.
|
||||
- Users must be able to override default behavior or extend it with minimal coupling.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
### 6. **Security and Isolation**
|
||||
- Scripts or instructions from external peers must be sandboxed and policy-checked.
|
||||
- Each execution context must enforce boundaries between data and logic.
|
||||
|
||||
### 7. **Resilience and Redundancy**
|
||||
- Failure of one peer or node must not impact others.
|
||||
- Communication must be asynchronous and fault-tolerant.
|
||||
|
||||
### 8. **Portability**
|
||||
- A peer’s logic and data must be portable across environments and host infrastructure.
|
||||
- No assumption of persistent centralized hosting.
|
||||
|
||||
### 9. **Transparency**
|
||||
- All logic must be auditable by its owner.
|
||||
- Communications between peers must be observable and traceable.
|
||||
|
||||
### 10. **Scalability**
|
||||
- The system must support large numbers of peer contexts, potentially hosted on shared infrastructure without compromising logical separation.
|
||||
|
||||
These requirements define the baseline for any system that claims to decentralize backend control and empower users to operate their own programmable, connected environments.
|
@ -0,0 +1,34 @@
|
||||
# Rethinking Backend Ownership
|
||||
|
||||
## Motivation
|
||||
|
||||
Modern applications are powered by backends that run on infrastructure and systems controlled by centralized entities. Whether it's social platforms, collaboration tools, or data-driven apps, the backend is almost always a black box — hosted, maintained, and operated by someone else.
|
||||
|
||||
This has profound implications:
|
||||
|
||||
- **Loss of autonomy:** Users are locked out of the logic, rules, and data structures that govern their digital experience.
|
||||
- **Opaque control:** Application behavior can change without the user’s consent — and often without visibility.
|
||||
- **Vendor lock-in:** Switching providers or migrating data is often non-trivial, risky, or impossible.
|
||||
- **Security and privacy risks:** Centralized backends present single points of failure and attack.
|
||||
|
||||
In this model, users are not participants in their computing environment — they are clients of someone else's backend.
|
||||
|
||||
## The Vision
|
||||
|
||||
The purpose of this initiative is to invert that dynamic. We aim to establish a paradigm where users and organizations **own and control their own backend logic and data**, without sacrificing connectivity, collaboration, or scalability.
|
||||
|
||||
This means:
|
||||
|
||||
- **Local authority:** Each user or organization should have full control over how their backend behaves — what code runs, what data is stored, and who can access it.
|
||||
- **Portable and interoperable:** Ownership must not mean isolation. User-owned backends should be able to interact with one another on equal footing.
|
||||
- **Transparent logic:** Application behavior should be visible, inspectable, and modifiable by the user.
|
||||
- **Delegation, not dependence:** Users should be able to cooperate and interact by delegating execution to each other — not by relying on a central server.
|
||||
|
||||
## What We Stand For
|
||||
|
||||
- **Agency:** You control your digital environment.
|
||||
- **Decentralization:** No central chokepoint for computation or data.
|
||||
- **Modularity:** Users compose their backend behavior, not inherit it from a monolith.
|
||||
- **Resilience:** Systems should degrade gracefully, fail independently, and recover without central orchestration.
|
||||
|
||||
This is about building a more equitable and open computing model — one where the backend serves you, not the other way around.
|
@ -0,0 +1 @@
|
||||
This is a sample text file.
|
@ -0,0 +1,50 @@
|
||||
# System Requirements Specification
|
||||
|
||||
## Objective
|
||||
|
||||
To define the core requirements for a system that fulfills the goals of decentralized backend ownership — enabling individuals and organizations to control, operate, and interact through their own backend environments without relying on centralized infrastructure.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### 1. **Isolated Execution Contexts**
|
||||
- Each user or peer must operate within a distinct, logically isolated execution context.
|
||||
- Contexts must not be able to interfere with each other's state or runtime.
|
||||
|
||||
### 2. **Cross-Context Communication**
|
||||
- Peers must be able to initiate interactions with other peers.
|
||||
- Communication must include origin metadata (who initiated it), and be authorized by the target context.
|
||||
|
||||
### 3. **Delegated Execution**
|
||||
- A peer must be able to send code or instructions to another peer for execution, under the recipient's policies.
|
||||
- The recipient must treat the execution as contextualized by the caller, but constrained by its own local rules.
|
||||
|
||||
### 4. **Ownership of Logic and Data**
|
||||
- Users must be able to inspect, modify, and extend the logic that governs their backend.
|
||||
- Data storage and access policies must be under the control of the peer.
|
||||
|
||||
### 5. **Composability and Modifiability**
|
||||
- System behavior must be defined by open, composable modules or scripts.
|
||||
- Users must be able to override default behavior or extend it with minimal coupling.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
### 6. **Security and Isolation**
|
||||
- Scripts or instructions from external peers must be sandboxed and policy-checked.
|
||||
- Each execution context must enforce boundaries between data and logic.
|
||||
|
||||
### 7. **Resilience and Redundancy**
|
||||
- Failure of one peer or node must not impact others.
|
||||
- Communication must be asynchronous and fault-tolerant.
|
||||
|
||||
### 8. **Portability**
|
||||
- A peer’s logic and data must be portable across environments and host infrastructure.
|
||||
- No assumption of persistent centralized hosting.
|
||||
|
||||
### 9. **Transparency**
|
||||
- All logic must be auditable by its owner.
|
||||
- Communications between peers must be observable and traceable.
|
||||
|
||||
### 10. **Scalability**
|
||||
- The system must support large numbers of peer contexts, potentially hosted on shared infrastructure without compromising logical separation.
|
||||
|
||||
These requirements define the baseline for any system that claims to decentralize backend control and empower users to operate their own programmable, connected environments.
|
Binary file not shown.
After Width: | Height: | Size: 4.5 MiB |
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
File diff suppressed because one or more lines are too long
34
examples/file_browser_demo/mock-server/mock_files/why.md
Normal file
34
examples/file_browser_demo/mock-server/mock_files/why.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Rethinking Backend Ownership
|
||||
|
||||
## Motivation
|
||||
|
||||
Modern applications are powered by backends that run on infrastructure and systems controlled by centralized entities. Whether it's social platforms, collaboration tools, or data-driven apps, the backend is almost always a black box — hosted, maintained, and operated by someone else.
|
||||
|
||||
This has profound implications:
|
||||
|
||||
- **Loss of autonomy:** Users are locked out of the logic, rules, and data structures that govern their digital experience.
|
||||
- **Opaque control:** Application behavior can change without the user’s consent — and often without visibility.
|
||||
- **Vendor lock-in:** Switching providers or migrating data is often non-trivial, risky, or impossible.
|
||||
- **Security and privacy risks:** Centralized backends present single points of failure and attack.
|
||||
|
||||
In this model, users are not participants in their computing environment — they are clients of someone else's backend.
|
||||
|
||||
## The Vision
|
||||
|
||||
The purpose of this initiative is to invert that dynamic. We aim to establish a paradigm where users and organizations **own and control their own backend logic and data**, without sacrificing connectivity, collaboration, or scalability.
|
||||
|
||||
This means:
|
||||
|
||||
- **Local authority:** Each user or organization should have full control over how their backend behaves — what code runs, what data is stored, and who can access it.
|
||||
- **Portable and interoperable:** Ownership must not mean isolation. User-owned backends should be able to interact with one another on equal footing.
|
||||
- **Transparent logic:** Application behavior should be visible, inspectable, and modifiable by the user.
|
||||
- **Delegation, not dependence:** Users should be able to cooperate and interact by delegating execution to each other — not by relying on a central server.
|
||||
|
||||
## What We Stand For
|
||||
|
||||
- **Agency:** You control your digital environment.
|
||||
- **Decentralization:** No central chokepoint for computation or data.
|
||||
- **Modularity:** Users compose their backend behavior, not inherit it from a monolith.
|
||||
- **Resilience:** Systems should degrade gracefully, fail independently, and recover without central orchestration.
|
||||
|
||||
This is about building a more equitable and open computing model — one where the backend serves you, not the other way around.
|
565
examples/file_browser_demo/mock-server/src/main.rs
Normal file
565
examples/file_browser_demo/mock-server/src/main.rs
Normal file
@ -0,0 +1,565 @@
|
||||
use axum::{
|
||||
extract::{DefaultBodyLimit, Path, Query},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Json, Response},
|
||||
routing::{delete, get, post},
|
||||
Router,
|
||||
};
|
||||
use walkdir::WalkDir;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path as StdPath, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// File/Directory item information
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct FileItem {
|
||||
name: String,
|
||||
path: String,
|
||||
is_directory: bool,
|
||||
size: Option<u64>,
|
||||
modified: Option<String>,
|
||||
hash: Option<String>,
|
||||
}
|
||||
|
||||
/// API response for directory listing
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ListResponse {
|
||||
contents: Vec<FileItem>,
|
||||
}
|
||||
|
||||
/// API response for errors
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ErrorResponse {
|
||||
error: String,
|
||||
}
|
||||
|
||||
/// API response for success messages
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SuccessResponse {
|
||||
message: String,
|
||||
}
|
||||
|
||||
/// Query parameters for listing
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ListQuery {
|
||||
recursive: Option<bool>,
|
||||
}
|
||||
|
||||
/// Mock server state
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
base_dir: PathBuf,
|
||||
// Simple upload tracking: upload_id -> (filename, file_path)
|
||||
uploads: Arc<Mutex<HashMap<String, (String, PathBuf)>>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
fn new() -> anyhow::Result<Self> {
|
||||
let base_dir = PathBuf::from("./mock_files");
|
||||
|
||||
// Create base directory if it doesn't exist
|
||||
fs::create_dir_all(&base_dir)?;
|
||||
|
||||
// Create some sample files and directories
|
||||
create_sample_files(&base_dir)?;
|
||||
|
||||
Ok(AppState {
|
||||
base_dir,
|
||||
uploads: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a safe path within the base directory
|
||||
fn get_safe_path(&self, user_path: &str) -> Option<PathBuf> {
|
||||
let user_path = if user_path.is_empty() || user_path == "." {
|
||||
"".to_string()
|
||||
} else {
|
||||
user_path.to_string()
|
||||
};
|
||||
|
||||
// Normalize path and prevent directory traversal
|
||||
let normalized = user_path.replace("..", "").replace("//", "/");
|
||||
let safe_path = self.base_dir.join(normalized);
|
||||
|
||||
// Ensure the path is within base directory
|
||||
if safe_path.starts_with(&self.base_dir) {
|
||||
Some(safe_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create sample files and directories for demo
|
||||
fn create_sample_files(base_dir: &StdPath) -> anyhow::Result<()> {
|
||||
let sample_dirs = ["documents", "images", "projects"];
|
||||
let sample_files = [
|
||||
("README.md", "# File Browser Demo\n\nThis is a sample file for testing the file browser component."),
|
||||
("sample.txt", "This is a sample text file."),
|
||||
("documents/report.md", "# Sample Report\n\nThis is a sample markdown report."),
|
||||
("documents/notes.txt", "Sample notes file content."),
|
||||
("images/placeholder.txt", "Placeholder for image files."),
|
||||
("projects/project1.md", "# Project 1\n\nSample project documentation."),
|
||||
("projects/config.json", r#"{"name": "sample-project", "version": "1.0.0"}"#),
|
||||
];
|
||||
|
||||
// Create sample directories
|
||||
for dir in &sample_dirs {
|
||||
let dir_path = base_dir.join(dir);
|
||||
fs::create_dir_all(dir_path)?;
|
||||
}
|
||||
|
||||
// Create sample files
|
||||
for (file_path, content) in &sample_files {
|
||||
let full_path = base_dir.join(file_path);
|
||||
if let Some(parent) = full_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(full_path, content)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert file metadata to FileItem
|
||||
fn file_to_item(path: &StdPath, base_dir: &StdPath) -> anyhow::Result<FileItem> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
let name = path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
let relative_path = path.strip_prefix(base_dir)
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| name.clone());
|
||||
|
||||
let modified = metadata.modified()
|
||||
.ok()
|
||||
.and_then(|time| DateTime::<Utc>::from(time).format("%Y-%m-%d %H:%M:%S").to_string().into());
|
||||
|
||||
Ok(FileItem {
|
||||
name,
|
||||
path: relative_path,
|
||||
is_directory: metadata.is_dir(),
|
||||
size: if metadata.is_file() { Some(metadata.len()) } else { None },
|
||||
modified,
|
||||
hash: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// List directory contents (root)
|
||||
/// GET /files/list/
|
||||
async fn list_root_directory(
|
||||
Query(params): Query<ListQuery>,
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
list_directory_impl("".to_string(), params, state).await
|
||||
}
|
||||
|
||||
/// List directory contents with path
|
||||
/// GET /files/list/<path>
|
||||
async fn list_directory(
|
||||
Path(path): Path<String>,
|
||||
Query(params): Query<ListQuery>,
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
list_directory_impl(path, params, state).await
|
||||
}
|
||||
|
||||
/// Internal implementation for directory listing
|
||||
async fn list_directory_impl(
|
||||
path: String,
|
||||
params: ListQuery,
|
||||
state: AppState,
|
||||
) -> impl IntoResponse {
|
||||
let safe_path = match state.get_safe_path(&path) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: "Invalid path".to_string() }),
|
||||
).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if !safe_path.exists() || !safe_path.is_dir() {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(ErrorResponse { error: "Directory not found".to_string() }),
|
||||
).into_response();
|
||||
}
|
||||
|
||||
let mut contents = Vec::new();
|
||||
|
||||
if params.recursive.unwrap_or(false) {
|
||||
// Recursive listing
|
||||
for entry in WalkDir::new(&safe_path) {
|
||||
if let Ok(entry) = entry {
|
||||
if entry.path() != safe_path {
|
||||
if let Ok(item) = file_to_item(entry.path(), &state.base_dir) {
|
||||
contents.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-recursive listing
|
||||
if let Ok(entries) = fs::read_dir(&safe_path) {
|
||||
for entry in entries.flatten() {
|
||||
if let Ok(item) = file_to_item(&entry.path(), &state.base_dir) {
|
||||
contents.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: directories first, then files, both alphabetically
|
||||
contents.sort_by(|a, b| {
|
||||
match (a.is_directory, b.is_directory) {
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
_ => a.name.cmp(&b.name),
|
||||
}
|
||||
});
|
||||
|
||||
Json(ListResponse { contents }).into_response()
|
||||
}
|
||||
|
||||
/// Create directory
|
||||
/// POST /files/dirs/<path>
|
||||
async fn create_directory(
|
||||
Path(path): Path<String>,
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
) -> Response {
|
||||
let safe_path = match state.get_safe_path(&path) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: "Invalid path".to_string() }),
|
||||
).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
match fs::create_dir_all(&safe_path) {
|
||||
Ok(_) => {
|
||||
info!("Created directory: {:?}", safe_path);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(SuccessResponse { message: "Directory created successfully".to_string() }),
|
||||
).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to create directory {:?}: {}", safe_path, e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: "Failed to create directory".to_string() }),
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete file or directory
|
||||
/// DELETE /files/delete/<path>
|
||||
async fn delete_item(
|
||||
Path(path): Path<String>,
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
) -> Response {
|
||||
let safe_path = match state.get_safe_path(&path) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: "Invalid path".to_string() }),
|
||||
).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if !safe_path.exists() {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(ErrorResponse { error: "File or directory not found".to_string() }),
|
||||
).into_response();
|
||||
}
|
||||
|
||||
let result = if safe_path.is_dir() {
|
||||
fs::remove_dir_all(&safe_path)
|
||||
} else {
|
||||
fs::remove_file(&safe_path)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
info!("Deleted: {:?}", safe_path);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(SuccessResponse { message: "Deleted successfully".to_string() }),
|
||||
).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to delete {:?}: {}", safe_path, e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: "Failed to delete".to_string() }),
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle TUS upload creation
|
||||
/// POST /files/upload
|
||||
/// POST /files/upload/<path> (for specific directory)
|
||||
async fn create_upload(
|
||||
headers: HeaderMap,
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
create_upload_impl(headers, state, None).await
|
||||
}
|
||||
|
||||
/// Handle TUS upload creation with path
|
||||
/// POST /files/upload/<path>
|
||||
async fn create_upload_with_path(
|
||||
Path(path): Path<String>,
|
||||
headers: HeaderMap,
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
create_upload_impl(headers, state, Some(path)).await
|
||||
}
|
||||
|
||||
/// Internal implementation for upload creation
|
||||
async fn create_upload_impl(
|
||||
headers: HeaderMap,
|
||||
state: AppState,
|
||||
target_path: Option<String>,
|
||||
) -> impl IntoResponse {
|
||||
let upload_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
// Get filename from Upload-Metadata header (base64 encoded)
|
||||
// TUS format: "filename <base64-encoded-filename>,type <base64-encoded-type>"
|
||||
let filename = headers
|
||||
.get("upload-metadata")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|metadata| {
|
||||
info!("Upload metadata received: {}", metadata);
|
||||
|
||||
// Parse TUS metadata format: "filename <base64>,type <base64>"
|
||||
for pair in metadata.split(',') {
|
||||
let parts: Vec<&str> = pair.trim().split_whitespace().collect();
|
||||
if parts.len() == 2 && parts[0] == "filename" {
|
||||
use base64::Engine;
|
||||
if let Ok(decoded_bytes) = base64::engine::general_purpose::STANDARD.decode(parts[1]) {
|
||||
if let Ok(decoded_filename) = String::from_utf8(decoded_bytes) {
|
||||
info!("Extracted filename: {}", decoded_filename);
|
||||
return Some(decoded_filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
warn!("Could not extract filename from metadata, using fallback: upload_{}", upload_id);
|
||||
format!("upload_{}", upload_id)
|
||||
});
|
||||
|
||||
// Determine target directory - use provided path or current directory
|
||||
let target_dir = if let Some(path) = target_path {
|
||||
if path.is_empty() {
|
||||
state.base_dir.clone()
|
||||
} else {
|
||||
state.base_dir.join(&path)
|
||||
}
|
||||
} else {
|
||||
state.base_dir.clone()
|
||||
};
|
||||
|
||||
// Create target directory if it doesn't exist
|
||||
if let Err(e) = fs::create_dir_all(&target_dir) {
|
||||
warn!("Failed to create target directory: {}", e);
|
||||
}
|
||||
|
||||
// Store upload metadata with preserved filename
|
||||
let upload_path = target_dir.join(&filename);
|
||||
|
||||
// Store the upload info for later use
|
||||
if let Ok(mut uploads) = state.uploads.lock() {
|
||||
uploads.insert(upload_id.clone(), (filename.clone(), upload_path));
|
||||
}
|
||||
|
||||
let mut response_headers = HeaderMap::new();
|
||||
response_headers.insert("Location", format!("/files/upload/{}", upload_id).parse().unwrap());
|
||||
response_headers.insert("Tus-Resumable", "1.0.0".parse().unwrap());
|
||||
|
||||
info!("Created upload with ID: {} for file: {}", upload_id, filename);
|
||||
(StatusCode::CREATED, response_headers, "")
|
||||
}
|
||||
|
||||
/// Handle TUS upload data
|
||||
/// PATCH /files/upload/<upload_id>
|
||||
async fn tus_upload_chunk(
|
||||
Path(upload_id): Path<String>,
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
_headers: HeaderMap,
|
||||
body: axum::body::Bytes,
|
||||
) -> impl IntoResponse {
|
||||
// Get upload info from tracking
|
||||
let upload_info = {
|
||||
if let Ok(uploads) = state.uploads.lock() {
|
||||
uploads.get(&upload_id).cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let (filename, file_path) = match upload_info {
|
||||
Some(info) => info,
|
||||
None => {
|
||||
warn!("Upload ID not found: {}", upload_id);
|
||||
return (StatusCode::NOT_FOUND, HeaderMap::new(), "").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Write the file data to disk
|
||||
match std::fs::write(&file_path, &body) {
|
||||
Ok(_) => {
|
||||
info!("Successfully saved file: {} ({} bytes)", filename, body.len());
|
||||
|
||||
// Clean up upload tracking
|
||||
if let Ok(mut uploads) = state.uploads.lock() {
|
||||
uploads.remove(&upload_id);
|
||||
}
|
||||
|
||||
let mut response_headers = HeaderMap::new();
|
||||
response_headers.insert("Tus-Resumable", "1.0.0".parse().unwrap());
|
||||
response_headers.insert("Upload-Offset", body.len().to_string().parse().unwrap());
|
||||
|
||||
(StatusCode::NO_CONTENT, response_headers, "").into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to save file {}: {}", filename, e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, HeaderMap::new(), "").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Download file
|
||||
/// GET /files/download/<path>
|
||||
async fn download_file(
|
||||
Path(path): Path<String>,
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let safe_path = match state.get_safe_path(&path) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: "Invalid path".to_string() }),
|
||||
).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if !safe_path.exists() || safe_path.is_dir() {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(ErrorResponse { error: "File not found".to_string() }),
|
||||
).into_response();
|
||||
}
|
||||
|
||||
match fs::read(&safe_path) {
|
||||
Ok(contents) => {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"Content-Disposition",
|
||||
format!("attachment; filename=\"{}\"",
|
||||
safe_path.file_name().unwrap_or_default().to_string_lossy())
|
||||
.parse().unwrap()
|
||||
);
|
||||
|
||||
(StatusCode::OK, headers, contents).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to read file {:?}: {}", safe_path, e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: "Failed to read file".to_string() }),
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Health check endpoint
|
||||
async fn health_check() -> impl IntoResponse {
|
||||
Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"message": "Mock file server is running"
|
||||
}))
|
||||
}
|
||||
|
||||
/// Root endpoint with API info
|
||||
async fn root() -> impl IntoResponse {
|
||||
Json(serde_json::json!({
|
||||
"name": "Mock File Server",
|
||||
"description": "A Rust mock server for testing the file browser component",
|
||||
"endpoints": {
|
||||
"GET /files/list/<path>": "List directory contents",
|
||||
"POST /files/dirs/<path>": "Create directory",
|
||||
"DELETE /files/delete/<path>": "Delete file/directory",
|
||||
"POST /files/upload": "Upload file (TUS protocol)",
|
||||
"PATCH /files/upload/<id>": "Upload file chunk",
|
||||
"GET /files/download/<path>": "Download file",
|
||||
"GET /health": "Health check"
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Initialize tracing
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Initialize app state
|
||||
let state = AppState::new()?;
|
||||
|
||||
info!("Base directory: {:?}", state.base_dir);
|
||||
|
||||
// Build the router
|
||||
let app = Router::new()
|
||||
.route("/", get(root))
|
||||
.route("/health", get(health_check))
|
||||
.route("/files/list/*path", get(list_directory))
|
||||
.route("/files/list/", get(list_root_directory))
|
||||
.route("/files/dirs/*path", post(create_directory))
|
||||
.route("/files/delete/*path", delete(delete_item))
|
||||
.route("/files/upload", post(create_upload))
|
||||
.route("/files/upload/to/*path", post(create_upload_with_path))
|
||||
.route("/files/upload/:upload_id", axum::routing::patch(tus_upload_chunk))
|
||||
.route("/files/download/*path", get(download_file))
|
||||
.layer(DefaultBodyLimit::max(500 * 1024 * 1024)) // 500MB limit for large file uploads
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
||||
// Start the server
|
||||
let port = std::env::var("PORT").unwrap_or_else(|_| "3001".to_string());
|
||||
let addr = format!("0.0.0.0:{}", port);
|
||||
|
||||
info!("🚀 Mock File Server starting on http://{}", addr);
|
||||
info!("📋 Available endpoints:");
|
||||
info!(" GET /files/list/<path> - List directory contents");
|
||||
info!(" POST /files/dirs/<path> - Create directory");
|
||||
info!(" DELETE /files/delete/<path> - Delete file/directory");
|
||||
info!(" POST /files/upload - Upload file (TUS)");
|
||||
info!(" GET /files/download/<path> - Download file");
|
||||
info!(" GET /health - Health check");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
21
examples/file_browser_demo/package.json
Normal file
21
examples/file_browser_demo/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "file-browser-demo-mock-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Mock server for file browser demo",
|
||||
"main": "mock-server.js",
|
||||
"scripts": {
|
||||
"start": "node mock-server.js",
|
||||
"dev": "nodemon mock-server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
},
|
||||
"keywords": ["file-browser", "mock-server", "demo"],
|
||||
"author": "Herocode Framework",
|
||||
"license": "MIT"
|
||||
}
|
107
examples/file_browser_demo/run-demo.sh
Executable file
107
examples/file_browser_demo/run-demo.sh
Executable file
@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run File Browser Demo Script
|
||||
# This script starts both the mock server and the WASM demo
|
||||
|
||||
set -e
|
||||
|
||||
echo "🎯 File Browser Demo Launcher"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
|
||||
# Function to cleanup background processes
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "🧹 Cleaning up..."
|
||||
if [ ! -z "$MOCK_SERVER_PID" ]; then
|
||||
kill $MOCK_SERVER_PID 2>/dev/null || true
|
||||
echo "✅ Mock server stopped"
|
||||
fi
|
||||
if [ ! -z "$TRUNK_PID" ]; then
|
||||
kill $TRUNK_PID 2>/dev/null || true
|
||||
echo "✅ Trunk dev server stopped"
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Set up signal handlers
|
||||
trap cleanup SIGINT SIGTERM
|
||||
|
||||
# Check dependencies
|
||||
echo "🔍 Checking dependencies..."
|
||||
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo "❌ Error: Rust/Cargo is not installed"
|
||||
echo "Please install Rust from https://rustup.rs/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v trunk &> /dev/null; then
|
||||
echo "❌ Error: Trunk is not installed"
|
||||
echo "Please install Trunk: cargo install trunk"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Dependencies OK"
|
||||
echo ""
|
||||
|
||||
# Start mock server in background
|
||||
echo "🚀 Starting mock server..."
|
||||
cd "$(dirname "$0")/mock-server"
|
||||
cargo build --release
|
||||
cargo run --release &
|
||||
MOCK_SERVER_PID=$!
|
||||
cd ..
|
||||
|
||||
# Wait a moment for server to start
|
||||
sleep 2
|
||||
|
||||
# Check if mock server is running
|
||||
if ! curl -s http://localhost:3001/health > /dev/null; then
|
||||
echo "❌ Error: Mock server failed to start"
|
||||
cleanup
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Mock server running on http://localhost:3001"
|
||||
echo ""
|
||||
|
||||
# Start trunk dev server
|
||||
echo "🌐 Starting WASM demo..."
|
||||
echo "Building and serving on http://localhost:8080"
|
||||
echo ""
|
||||
echo "📋 Demo Features:"
|
||||
echo " • File/directory listing with navigation"
|
||||
echo " • Create new directories"
|
||||
echo " • Upload files with progress tracking"
|
||||
echo " • Download files"
|
||||
echo " • Delete files and directories"
|
||||
echo " • Responsive Bootstrap UI"
|
||||
echo ""
|
||||
echo "💡 Use Ctrl+C to stop both servers"
|
||||
echo ""
|
||||
|
||||
trunk serve --port 8080 &
|
||||
TRUNK_PID=$!
|
||||
|
||||
# Wait for trunk to start
|
||||
sleep 3
|
||||
|
||||
# Open browser (optional)
|
||||
if command -v open &> /dev/null; then
|
||||
echo "🌍 Opening browser..."
|
||||
open http://localhost:8080
|
||||
elif command -v xdg-open &> /dev/null; then
|
||||
echo "🌍 Opening browser..."
|
||||
xdg-open http://localhost:8080
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 Demo is ready!"
|
||||
echo " 📱 Frontend: http://localhost:8080"
|
||||
echo " 🔧 Backend: http://localhost:3001"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop all servers"
|
||||
|
||||
# Wait for user to stop
|
||||
wait
|
39
examples/file_browser_demo/run-mock-server.sh
Executable file
39
examples/file_browser_demo/run-mock-server.sh
Executable file
@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run Mock Server Script
|
||||
# This script starts the Rust mock server for testing the file browser component
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting Mock File Server..."
|
||||
echo "📁 This server provides the same API as src/files.py for testing"
|
||||
echo ""
|
||||
|
||||
# Change to mock server directory
|
||||
cd "$(dirname "$0")/mock-server"
|
||||
|
||||
# Check if Rust is installed
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo "❌ Error: Rust/Cargo is not installed"
|
||||
echo "Please install Rust from https://rustup.rs/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build and run the mock server
|
||||
echo "🔨 Building mock server..."
|
||||
cargo build --release
|
||||
|
||||
echo "🌐 Starting server on http://localhost:3001"
|
||||
echo "📋 Available endpoints:"
|
||||
echo " GET /files/list/<path> - List directory contents"
|
||||
echo " POST /files/dirs/<path> - Create directory"
|
||||
echo " DELETE /files/delete/<path> - Delete file/directory"
|
||||
echo " POST /files/upload - Upload file (TUS)"
|
||||
echo " GET /files/download/<path> - Download file"
|
||||
echo " GET /health - Health check"
|
||||
echo ""
|
||||
echo "💡 Use Ctrl+C to stop the server"
|
||||
echo ""
|
||||
|
||||
# Run the server
|
||||
cargo run --release
|
31
examples/file_browser_demo/serve.sh
Executable file
31
examples/file_browser_demo/serve.sh
Executable file
@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# serve.sh - Build optimized WASM and serve with Trunk
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 Building optimized WASM bundle..."
|
||||
trunk build --release
|
||||
|
||||
echo "📦 Checking bundle sizes..."
|
||||
if [ -d "dist" ]; then
|
||||
echo "Bundle sizes:"
|
||||
find dist -name "*.wasm" -exec ls -lh {} \; | awk '{print " WASM: " $5 " - " $9}'
|
||||
find dist -name "*.js" -exec ls -lh {} \; | awk '{print " JS: " $5 " - " $9}'
|
||||
find dist -name "*.css" -exec ls -lh {} \; | awk '{print " CSS: " $5 " - " $9}'
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "🚀 Starting Trunk development server..."
|
||||
echo "📍 Server will be available at: http://127.0.0.1:8080"
|
||||
echo ""
|
||||
echo "💡 Tips:"
|
||||
echo " - Make sure your Flask backend is running on http://127.0.0.1:5000"
|
||||
echo " - Check CORS configuration in your backend"
|
||||
echo " - Upload files will use TUS protocol for resumable uploads"
|
||||
echo ""
|
||||
echo "⏹️ Press Ctrl+C to stop the server"
|
||||
echo ""
|
||||
|
||||
# Start Trunk serve
|
||||
trunk serve --release
|
87
examples/file_browser_demo/src/main.rs
Normal file
87
examples/file_browser_demo/src/main.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use yew::prelude::*;
|
||||
use framework::components::{FileBrowser, FileBrowserConfig};
|
||||
|
||||
#[function_component(FileBrowserPage)]
|
||||
fn file_browser_page() -> Html {
|
||||
let config = FileBrowserConfig {
|
||||
base_endpoint: "http://localhost:3001/files".to_string(),
|
||||
max_file_size: 100 * 1024 * 1024, // 100MB
|
||||
chunk_size: 1024 * 1024, // 1MB
|
||||
initial_path: "".to_string(),
|
||||
show_upload: true,
|
||||
show_download: true,
|
||||
show_delete: true,
|
||||
show_create_dir: true,
|
||||
css_classes: "container-fluid".to_string(),
|
||||
theme: "light".to_string(),
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="app">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand mb-0 h1">
|
||||
<i class="bi bi-folder2-open"></i>
|
||||
{" File Browser Demo"}
|
||||
</span>
|
||||
<div class="navbar-text">
|
||||
{"Powered by Uppy.js & TUS Protocol"}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="bi bi-hdd-stack"></i>
|
||||
{" File System Browser"}
|
||||
</h5>
|
||||
<small class="text-muted">
|
||||
{"Browse, upload, download, and manage files with resumable uploads"}
|
||||
</small>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<FileBrowser ..config />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-5 py-4 bg-light text-center text-muted">
|
||||
<div class="container">
|
||||
<p class="mb-1">
|
||||
{"File Browser Component Demo - Built with "}
|
||||
<a href="https://yew.rs" target="_blank" class="text-decoration-none">{"Yew"}</a>
|
||||
{", "}
|
||||
<a href="https://uppy.io" target="_blank" class="text-decoration-none">{"Uppy.js"}</a>
|
||||
{", and "}
|
||||
<a href="https://tus.io" target="_blank" class="text-decoration-none">{"TUS Protocol"}</a>
|
||||
</p>
|
||||
<small>{"Compiled to WebAssembly for maximum performance"}</small>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
html! { <FileBrowserPage /> }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Set up panic hook for better error messages in development
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
// Use wee_alloc as the global allocator for smaller WASM binary size
|
||||
#[cfg(feature = "wee_alloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
0
examples/website/build.sh
Normal file → Executable file
0
examples/website/build.sh
Normal file → Executable file
@ -6,7 +6,8 @@ use crate::router::{Route, switch};
|
||||
use crate::console::{expose_to_console, log_console_examples};
|
||||
|
||||
pub struct App {
|
||||
ws_manager: WsManager,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
ws_manager: WsManager,
|
||||
}
|
||||
|
||||
pub enum AppMsg {
|
||||
@ -18,25 +19,36 @@ impl Component for App {
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
let ws_manager = WsManager::builder()
|
||||
.add_server_url("ws://localhost:8080".to_string())
|
||||
.add_server_url("ws://localhost:8081".to_string())
|
||||
.add_server_url("ws://localhost:8443/ws".to_string())
|
||||
.build();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let ws_manager = WsManager::builder()
|
||||
.add_server_url("ws://localhost:8080".to_string())
|
||||
.add_server_url("ws://localhost:8081".to_string())
|
||||
.add_server_url("ws://localhost:8443/ws".to_string())
|
||||
.build();
|
||||
|
||||
// Expose WebSocket manager to browser console
|
||||
expose_to_console(ws_manager.clone());
|
||||
log_console_examples();
|
||||
// Expose WebSocket manager to browser console
|
||||
expose_to_console(ws_manager.clone());
|
||||
log_console_examples();
|
||||
|
||||
// Clone the manager to move it into the async block
|
||||
let manager_clone = ws_manager.clone();
|
||||
spawn_local(async move {
|
||||
if let Err(e) = manager_clone.connect().await {
|
||||
log::error!("Failed to connect WebSocket manager: {:?}", e);
|
||||
}
|
||||
});
|
||||
// Clone the manager to move it into the async block
|
||||
let manager_clone = ws_manager.clone();
|
||||
spawn_local(async move {
|
||||
if let Err(e) = manager_clone.connect().await {
|
||||
log::error!("Failed to connect WebSocket manager: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Self { ws_manager }
|
||||
Self { ws_manager }
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
// For WASM builds, just log console examples
|
||||
log_console_examples();
|
||||
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> bool {
|
||||
@ -44,9 +56,17 @@ impl Component for App {
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
let ws_manager_for_switch = self.ws_manager.clone();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let switch_render = {
|
||||
let ws_manager_for_switch = self.ws_manager.clone();
|
||||
Callback::from(move |route: Route| {
|
||||
switch(route, ws_manager_for_switch.clone())
|
||||
})
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let switch_render = Callback::from(move |route: Route| {
|
||||
switch(route, ws_manager_for_switch.clone())
|
||||
switch(route)
|
||||
});
|
||||
|
||||
html! {
|
||||
|
208
examples/widget_example/README.md
Normal file
208
examples/widget_example/README.md
Normal file
@ -0,0 +1,208 @@
|
||||
# FileBrowser Widget
|
||||
|
||||
A WebAssembly-based file browser widget that can be embedded in any web application.
|
||||
|
||||
## Features
|
||||
|
||||
- File and directory browsing
|
||||
- File upload with progress tracking (using TUS protocol)
|
||||
- File download
|
||||
- Directory creation and deletion
|
||||
- File editing (markdown with live preview, text files)
|
||||
|
||||
## Running the Example
|
||||
|
||||
1. **Start a local server** (required for WASM):
|
||||
```bash
|
||||
python3 -m http.server 8081
|
||||
# or
|
||||
npx serve .
|
||||
```
|
||||
|
||||
2. **Start the mock backend** (in another terminal):
|
||||
```bash
|
||||
cd ../file_browser_demo
|
||||
cargo run --bin mock_server
|
||||
```
|
||||
|
||||
3. **Open the example**:
|
||||
- Navigate to `http://localhost:8081`
|
||||
- The widget will load with a configuration panel
|
||||
- Try different settings and see them applied in real-time
|
||||
|
||||
## Key Features Demonstrated
|
||||
|
||||
### Runtime Configuration
|
||||
The example shows how to configure the widget at runtime without rebuilding:
|
||||
|
||||
```javascript
|
||||
// Create base configuration
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
|
||||
// Apply runtime settings using corrected method names
|
||||
config.setTheme('light'); // Theme selection
|
||||
config.setMaxFileSize(100 * 1024 * 1024); // 100MB limit
|
||||
config.setShowUpload(true); // Enable upload
|
||||
config.setShowDownload(true); // Enable download
|
||||
config.setShowDelete(false); // Disable delete
|
||||
config.setInitialPath('documents/'); // Start in documents folder
|
||||
|
||||
// Create widget with configuration
|
||||
const widget = create_file_browser_widget('container-id', config);
|
||||
```
|
||||
|
||||
### Dynamic Reconfiguration
|
||||
The widget can be recreated with new settings:
|
||||
|
||||
```javascript
|
||||
function updateWidget() {
|
||||
// Destroy existing widget
|
||||
if (currentWidget) {
|
||||
currentWidget.destroy();
|
||||
}
|
||||
|
||||
// Create new widget with updated config
|
||||
const newConfig = create_default_config(newEndpoint);
|
||||
newConfig.setTheme(selectedTheme);
|
||||
currentWidget = create_file_browser_widget('container', newConfig);
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
The example includes comprehensive error handling:
|
||||
|
||||
- WASM initialization errors
|
||||
- Browser compatibility checks
|
||||
- Widget creation failures
|
||||
- Network connectivity issues
|
||||
|
||||
## Widget API Reference
|
||||
|
||||
### Core Functions
|
||||
|
||||
```javascript
|
||||
// Initialize WASM module (call once)
|
||||
await init();
|
||||
|
||||
// Create default configuration
|
||||
const config = create_default_config(baseEndpoint);
|
||||
|
||||
// Create widget instance
|
||||
const widget = create_file_browser_widget(containerId, config);
|
||||
|
||||
// Utility functions
|
||||
const version = get_version();
|
||||
const isCompatible = check_browser_compatibility();
|
||||
```
|
||||
|
||||
### Configuration Methods
|
||||
|
||||
```javascript
|
||||
config.setTheme(theme); // 'light' | 'dark'
|
||||
config.setMaxFileSize(bytes); // Number in bytes
|
||||
config.setShowUpload(enabled); // Boolean
|
||||
config.setShowDownload(enabled); // Boolean
|
||||
config.setShowDelete(enabled); // Boolean
|
||||
config.setCssClasses(classes); // String of CSS classes
|
||||
config.setInitialPath(path); // String path
|
||||
```
|
||||
|
||||
### Widget Handle Methods
|
||||
|
||||
```javascript
|
||||
widget.destroy(); // Clean up widget
|
||||
// Note: Currently no update method - recreate widget for config changes
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Styling
|
||||
```javascript
|
||||
config.setCssClasses('my-custom-theme dark-mode');
|
||||
```
|
||||
|
||||
### Multiple Widgets
|
||||
```javascript
|
||||
const widget1 = create_file_browser_widget('container1', config1);
|
||||
const widget2 = create_file_browser_widget('container2', config2);
|
||||
```
|
||||
|
||||
### Integration with Frameworks
|
||||
|
||||
**React:**
|
||||
```jsx
|
||||
function FileBrowserComponent({ endpoint }) {
|
||||
const containerRef = useRef();
|
||||
const widgetRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
async function initWidget() {
|
||||
await init();
|
||||
const config = create_default_config(endpoint);
|
||||
widgetRef.current = create_file_browser_widget(
|
||||
containerRef.current,
|
||||
config
|
||||
);
|
||||
}
|
||||
initWidget();
|
||||
|
||||
return () => widgetRef.current?.destroy();
|
||||
}, [endpoint]);
|
||||
|
||||
return <div ref={containerRef} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Vue:**
|
||||
```vue
|
||||
<template>
|
||||
<div ref="container"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async mounted() {
|
||||
await init();
|
||||
const config = create_default_config(this.endpoint);
|
||||
this.widget = create_file_browser_widget(this.$refs.container, config);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.widget?.destroy();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"config.setTheme is not a function"**
|
||||
- Ensure you're using the latest widget build
|
||||
- Check that WASM module is properly initialized
|
||||
|
||||
2. **Widget not appearing**
|
||||
- Verify container element exists
|
||||
- Check browser console for errors
|
||||
- Ensure WASM files are served correctly
|
||||
|
||||
3. **Backend connection errors**
|
||||
- Verify backend is running on specified endpoint
|
||||
- Check CORS configuration
|
||||
- Ensure all required API endpoints are implemented
|
||||
|
||||
### Debug Mode
|
||||
```javascript
|
||||
// Enable debug logging
|
||||
console.log('Widget version:', get_version());
|
||||
console.log('Browser compatible:', check_browser_compatibility());
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- **Initial Load**: ~368KB total (WASM + JS)
|
||||
- **Runtime Memory**: ~2-5MB depending on file list size
|
||||
- **Startup Time**: ~100-300ms on modern browsers
|
||||
- **File Operations**: Near-native performance via WASM
|
||||
|
||||
The widget is optimized for production use with minimal overhead.
|
125
examples/widget_example/example.html
Normal file
125
examples/widget_example/example.html
Normal file
@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FileBrowser Widget Example</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
body { padding: 20px; }
|
||||
.widget-container {
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>FileBrowser Widget Example</h1>
|
||||
<p>This demonstrates how to embed the FileBrowser widget in your website.</p>
|
||||
|
||||
<div class="widget-container">
|
||||
<h3>File Browser Widget</h3>
|
||||
<div id="file-browser-widget"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4>Configuration</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="endpoint" class="form-label">Base Endpoint:</label>
|
||||
<input type="text" id="endpoint" class="form-control" value="http://localhost:3001/files">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="theme" class="form-label">Theme:</label>
|
||||
<select id="theme" class="form-select">
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="show-upload" checked>
|
||||
<label class="form-check-label" for="show-upload">Show Upload</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="show-download" checked>
|
||||
<label class="form-check-label" for="show-download">Show Download</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="show-delete" checked>
|
||||
<label class="form-check-label" for="show-delete">Show Delete</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="recreate-widget" class="btn btn-primary mt-3">Recreate Widget</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import init, {
|
||||
create_file_browser_widget,
|
||||
create_default_config,
|
||||
check_browser_compatibility,
|
||||
get_version
|
||||
} from './file_browser_widget.js';
|
||||
|
||||
let currentWidget = null;
|
||||
|
||||
async function initWidget() {
|
||||
await init();
|
||||
|
||||
console.log('FileBrowser Widget version:', get_version());
|
||||
|
||||
if (!check_browser_compatibility()) {
|
||||
alert('Your browser is not compatible with this widget');
|
||||
return;
|
||||
}
|
||||
|
||||
createWidget();
|
||||
}
|
||||
|
||||
function createWidget() {
|
||||
// Destroy existing widget
|
||||
if (currentWidget) {
|
||||
currentWidget.destroy();
|
||||
currentWidget = null;
|
||||
}
|
||||
|
||||
// Clear container
|
||||
const container = document.getElementById('file-browser-widget');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Get configuration from form
|
||||
const config = create_default_config(document.getElementById('endpoint').value);
|
||||
config.set_theme(document.getElementById('theme').value);
|
||||
config.set_show_upload(document.getElementById('show-upload').checked);
|
||||
config.set_show_download(document.getElementById('show-download').checked);
|
||||
config.set_show_delete(document.getElementById('show-delete').checked);
|
||||
|
||||
try {
|
||||
currentWidget = create_file_browser_widget('file-browser-widget', config);
|
||||
console.log('Widget created successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to create widget:', error);
|
||||
container.innerHTML = `<div class="alert alert-danger">Failed to create widget: ${error}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('recreate-widget').addEventListener('click', createWidget);
|
||||
|
||||
// Initialize when page loads
|
||||
initWidget();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
112
examples/widget_example/file_browser_widget.d.ts
vendored
Normal file
112
examples/widget_example/file_browser_widget.d.ts
vendored
Normal file
@ -0,0 +1,112 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export function main(): void;
|
||||
/**
|
||||
* Create and mount a FileBrowser widget to the specified DOM element
|
||||
*/
|
||||
export function create_file_browser_widget(container_id: string, config: JSWidgetConfig): FileBrowserWidgetHandle;
|
||||
/**
|
||||
* Create and mount a FileBrowser widget to a specific DOM element
|
||||
*/
|
||||
export function create_file_browser_widget_on_element(element: Element, config: JSWidgetConfig): FileBrowserWidgetHandle;
|
||||
/**
|
||||
* Utility function to create a default configuration
|
||||
*/
|
||||
export function create_default_config(base_endpoint: string): JSWidgetConfig;
|
||||
/**
|
||||
* Get version information
|
||||
*/
|
||||
export function get_version(): string;
|
||||
/**
|
||||
* Check if the widget is compatible with the current browser
|
||||
*/
|
||||
export function check_browser_compatibility(): boolean;
|
||||
/**
|
||||
* Handle for managing the widget instance
|
||||
*/
|
||||
export class FileBrowserWidgetHandle {
|
||||
private constructor();
|
||||
free(): void;
|
||||
/**
|
||||
* Destroy the widget instance
|
||||
*/
|
||||
destroy(): void;
|
||||
/**
|
||||
* Update the widget configuration
|
||||
*/
|
||||
update_config(_config: JSWidgetConfig): void;
|
||||
}
|
||||
/**
|
||||
* JavaScript-compatible configuration wrapper
|
||||
*/
|
||||
export class JSWidgetConfig {
|
||||
free(): void;
|
||||
constructor(base_endpoint: string);
|
||||
setMaxFileSize(size: bigint): void;
|
||||
setShowUpload(show: boolean): void;
|
||||
setShowDownload(show: boolean): void;
|
||||
setShowDelete(show: boolean): void;
|
||||
setTheme(theme: string): void;
|
||||
setCssClasses(classes: string): void;
|
||||
setInitialPath(path: string): void;
|
||||
}
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly main: () => void;
|
||||
readonly __wbg_jswidgetconfig_free: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_new: (a: number, b: number) => number;
|
||||
readonly jswidgetconfig_setMaxFileSize: (a: number, b: bigint) => void;
|
||||
readonly jswidgetconfig_setShowUpload: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_setShowDownload: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_setShowDelete: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_setTheme: (a: number, b: number, c: number) => void;
|
||||
readonly jswidgetconfig_setCssClasses: (a: number, b: number, c: number) => void;
|
||||
readonly jswidgetconfig_setInitialPath: (a: number, b: number, c: number) => void;
|
||||
readonly __wbg_filebrowserwidgethandle_free: (a: number, b: number) => void;
|
||||
readonly filebrowserwidgethandle_destroy: (a: number) => void;
|
||||
readonly filebrowserwidgethandle_update_config: (a: number, b: number) => void;
|
||||
readonly create_file_browser_widget: (a: number, b: number, c: number) => [number, number, number];
|
||||
readonly create_file_browser_widget_on_element: (a: any, b: number) => [number, number, number];
|
||||
readonly create_default_config: (a: number, b: number) => number;
|
||||
readonly get_version: () => [number, number];
|
||||
readonly check_browser_compatibility: () => number;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly __externref_table_alloc: () => number;
|
||||
readonly __wbindgen_export_2: WebAssembly.Table;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __externref_drop_slice: (a: number, b: number) => void;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_export_7: WebAssembly.Table;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly _dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h126b2208f8e42866: (a: number, b: number) => void;
|
||||
readonly closure28_externref_shim: (a: number, b: number, c: any, d: any) => void;
|
||||
readonly closure25_externref_shim: (a: number, b: number, c: any, d: any, e: any) => void;
|
||||
readonly closure52_externref_shim: (a: number, b: number, c: any) => void;
|
||||
readonly closure62_externref_shim: (a: number, b: number, c: any) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
1082
examples/widget_example/file_browser_widget.js
Normal file
1082
examples/widget_example/file_browser_widget.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
examples/widget_example/file_browser_widget_bg.wasm
Normal file
BIN
examples/widget_example/file_browser_widget_bg.wasm
Normal file
Binary file not shown.
262
examples/widget_example/index.html
Normal file
262
examples/widget_example/index.html
Normal file
@ -0,0 +1,262 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FileBrowser Widget Example</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<script src="./uppy.min.js"></script>
|
||||
<link href="./uppy.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.widget-container {
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.config-panel {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.status-success { background-color: #28a745; }
|
||||
.status-error { background-color: #dc3545; }
|
||||
.status-loading { background-color: #ffc107; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">
|
||||
<i class="bi bi-folder2-open"></i>
|
||||
FileBrowser Widget Example
|
||||
</h1>
|
||||
<p class="lead">This demonstrates how to embed the FileBrowser widget in your website with runtime configuration.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="config-panel">
|
||||
<h4>
|
||||
<i class="bi bi-gear"></i>
|
||||
Configuration
|
||||
</h4>
|
||||
<div class="mb-3">
|
||||
<label for="endpoint" class="form-label">Base Endpoint:</label>
|
||||
<input type="text" id="endpoint" class="form-control" value="http://localhost:3001/files">
|
||||
<div class="form-text">Backend API endpoint for file operations</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="theme" class="form-label">Theme:</label>
|
||||
<select id="theme" class="form-select">
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="max-file-size" class="form-label">Max File Size (MB):</label>
|
||||
<input type="number" id="max-file-size" class="form-control" value="100" min="1" max="1000">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Features:</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="show-upload" checked>
|
||||
<label class="form-check-label" for="show-upload">Show Upload</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="show-download" checked>
|
||||
<label class="form-check-label" for="show-download">Show Download</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="show-delete" checked>
|
||||
<label class="form-check-label" for="show-delete">Show Delete</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="initial-path" class="form-label">Initial Path:</label>
|
||||
<input type="text" id="initial-path" class="form-control" placeholder="e.g., documents/">
|
||||
</div>
|
||||
|
||||
<button id="recreate-widget" class="btn btn-primary w-100">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
Apply Configuration
|
||||
</button>
|
||||
|
||||
<div class="mt-3">
|
||||
<div id="status" class="small">
|
||||
<span class="status-indicator status-loading"></span>
|
||||
<span id="status-text">Initializing...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-panel">
|
||||
<h5>
|
||||
<i class="bi bi-info-circle"></i>
|
||||
Widget Info
|
||||
</h5>
|
||||
<div class="small">
|
||||
<div><strong>Version:</strong> <span id="widget-version">Loading...</span></div>
|
||||
<div><strong>Browser Compatible:</strong> <span id="browser-compat">Checking...</span></div>
|
||||
<div><strong>WASM Size:</strong> ~329KB</div>
|
||||
<div><strong>JS Size:</strong> ~39KB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="widget-container">
|
||||
<h3>
|
||||
<i class="bi bi-hdd-stack"></i>
|
||||
File Browser Widget
|
||||
</h3>
|
||||
<p class="text-muted mb-3">The widget will appear below once initialized:</p>
|
||||
<div id="file-browser-widget"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import init, {
|
||||
create_file_browser_widget,
|
||||
create_default_config,
|
||||
check_browser_compatibility,
|
||||
get_version
|
||||
} from './file_browser_widget.js';
|
||||
|
||||
let currentWidget = null;
|
||||
let isInitialized = false;
|
||||
|
||||
function updateStatus(text, type = 'loading') {
|
||||
const statusElement = document.getElementById('status-text');
|
||||
const indicatorElement = document.querySelector('.status-indicator');
|
||||
|
||||
statusElement.textContent = text;
|
||||
indicatorElement.className = `status-indicator status-${type}`;
|
||||
}
|
||||
|
||||
async function initWidget() {
|
||||
try {
|
||||
updateStatus('Loading WASM module...', 'loading');
|
||||
await init();
|
||||
|
||||
updateStatus('Checking compatibility...', 'loading');
|
||||
const version = get_version();
|
||||
const isCompatible = check_browser_compatibility();
|
||||
|
||||
document.getElementById('widget-version').textContent = version;
|
||||
document.getElementById('browser-compat').textContent = isCompatible ? 'Yes ✓' : 'No ✗';
|
||||
|
||||
if (!isCompatible) {
|
||||
updateStatus('Browser not compatible', 'error');
|
||||
document.getElementById('file-browser-widget').innerHTML =
|
||||
'<div class="alert alert-danger">Your browser is not compatible with this widget</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
updateStatus('Ready', 'success');
|
||||
createWidget();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize widget:', error);
|
||||
updateStatus(`Initialization failed: ${error.message}`, 'error');
|
||||
document.getElementById('file-browser-widget').innerHTML =
|
||||
`<div class="alert alert-danger">Failed to initialize: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function createWidget() {
|
||||
if (!isInitialized) {
|
||||
updateStatus('Widget not initialized', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
updateStatus('Creating widget...', 'loading');
|
||||
|
||||
// Destroy existing widget
|
||||
if (currentWidget) {
|
||||
currentWidget.destroy();
|
||||
currentWidget = null;
|
||||
}
|
||||
|
||||
// Clear container
|
||||
const container = document.getElementById('file-browser-widget');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Get configuration from form
|
||||
const config = create_default_config(document.getElementById('endpoint').value);
|
||||
|
||||
// Apply configuration using the corrected method names
|
||||
config.setTheme(document.getElementById('theme').value);
|
||||
config.setMaxFileSize(parseInt(document.getElementById('max-file-size').value) * 1024 * 1024);
|
||||
config.setShowUpload(document.getElementById('show-upload').checked);
|
||||
config.setShowDownload(document.getElementById('show-download').checked);
|
||||
config.setShowDelete(document.getElementById('show-delete').checked);
|
||||
|
||||
const initialPath = document.getElementById('initial-path').value.trim();
|
||||
if (initialPath) {
|
||||
config.setInitialPath(initialPath);
|
||||
}
|
||||
|
||||
// Create widget
|
||||
currentWidget = create_file_browser_widget('file-browser-widget', config);
|
||||
updateStatus('Widget ready', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create widget:', error);
|
||||
updateStatus(`Widget creation failed: ${error.message}`, 'error');
|
||||
document.getElementById('file-browser-widget').innerHTML =
|
||||
`<div class="alert alert-danger">Failed to create widget: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('recreate-widget').addEventListener('click', createWidget);
|
||||
|
||||
// Auto-recreate on configuration changes
|
||||
['endpoint', 'theme', 'max-file-size', 'show-upload', 'show-download', 'show-delete', 'initial-path'].forEach(id => {
|
||||
const element = document.getElementById(id);
|
||||
if (element.type === 'checkbox') {
|
||||
element.addEventListener('change', () => {
|
||||
if (isInitialized) createWidget();
|
||||
});
|
||||
} else {
|
||||
element.addEventListener('input', () => {
|
||||
if (isInitialized) {
|
||||
clearTimeout(element.debounceTimer);
|
||||
element.debounceTimer = setTimeout(createWidget, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize when page loads
|
||||
initWidget();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
11
examples/widget_example/uppy.min.css
vendored
Normal file
11
examples/widget_example/uppy.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
69
examples/widget_example/uppy.min.js
vendored
Normal file
69
examples/widget_example/uppy.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
178
examples/ws_manager_demo.rs
Normal file
178
examples/ws_manager_demo.rs
Normal file
@ -0,0 +1,178 @@
|
||||
//! WebSocket Manager Demo
|
||||
//!
|
||||
//! This example demonstrates how to use the WsManager to connect to multiple
|
||||
//! WebSocket servers, authenticate, and execute scripts on connected clients.
|
||||
//!
|
||||
//! # Prerequisites
|
||||
//!
|
||||
//! 1. Start one or more Hero WebSocket servers:
|
||||
//! ```bash
|
||||
//! # Terminal 1
|
||||
//! cd hero/interfaces/websocket/server
|
||||
//! cargo run --example circle_auth_demo -- --port 3030
|
||||
//!
|
||||
//! # Terminal 2 (optional)
|
||||
//! cargo run --example circle_auth_demo -- --port 3031
|
||||
//! ```
|
||||
//!
|
||||
//! 2. Run this example:
|
||||
//! ```bash
|
||||
//! cd framework
|
||||
//! cargo run --example ws_manager_demo
|
||||
//! ```
|
||||
|
||||
use framework::WsManager;
|
||||
use log::{info, warn, error};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize logging
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
info!("🚀 Starting WebSocket Manager Demo");
|
||||
|
||||
// Example private key (in real usage, load from secure storage)
|
||||
let private_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
|
||||
// Create a manager with multiple servers
|
||||
let manager = WsManager::builder()
|
||||
.private_key(private_key.to_string())
|
||||
.add_server_url("ws://localhost:3030".to_string())
|
||||
.add_server_url("ws://localhost:3031".to_string())
|
||||
.build();
|
||||
|
||||
info!("📡 Connecting to WebSocket servers...");
|
||||
|
||||
// Connect to all configured servers
|
||||
match manager.connect().await {
|
||||
Ok(()) => info!("✅ Successfully initiated connections"),
|
||||
Err(e) => {
|
||||
error!("❌ Failed to connect: {}", e);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Give connections time to establish
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
|
||||
// Check connection status
|
||||
info!("🔍 Checking connection status...");
|
||||
let statuses = manager.get_all_connection_statuses();
|
||||
for (url, status) in &statuses {
|
||||
info!(" {} -> {}", url, status);
|
||||
}
|
||||
|
||||
// Demonstrate script execution on a specific server
|
||||
info!("🎯 Executing script on specific server...");
|
||||
let script = r#"
|
||||
console.log("Hello from WebSocket Manager!");
|
||||
return "Script executed successfully";
|
||||
"#;
|
||||
|
||||
if let Some(future) = manager.with_client("ws://localhost:3030", |client| {
|
||||
info!(" 📤 Sending script to ws://localhost:3030");
|
||||
client.play(script.to_string())
|
||||
}) {
|
||||
let result = future.await;
|
||||
match result {
|
||||
Ok(output) => info!(" ✅ Script output: {}", output),
|
||||
Err(e) => warn!(" ⚠️ Script error: {}", e),
|
||||
}
|
||||
} else {
|
||||
warn!(" ❌ Server ws://localhost:3030 is not connected");
|
||||
}
|
||||
|
||||
// Demonstrate operations on all connected clients
|
||||
info!("🌐 Executing operations on all connected clients...");
|
||||
|
||||
let ping_script = "return 'pong from ' + new Date().toISOString();";
|
||||
|
||||
// Get list of connected URLs first
|
||||
let connected_urls = manager.get_connected_urls();
|
||||
|
||||
for url in connected_urls {
|
||||
info!(" 📤 Pinging {}", url);
|
||||
|
||||
if let Some(future) = manager.with_client(&url, |client| {
|
||||
client.play(ping_script.to_string())
|
||||
}) {
|
||||
match future.await {
|
||||
Ok(output) => info!(" ✅ {} responded: {}", url, output),
|
||||
Err(e) => warn!(" ⚠️ {} error: {}", url, e),
|
||||
}
|
||||
} else {
|
||||
warn!(" ❌ {} is not connected", url);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for async operations to complete
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
|
||||
// Demonstrate authentication status check
|
||||
info!("🔐 Checking authentication status...");
|
||||
// Note: For complex async operations, you may need to handle them differently
|
||||
// due to Rust's lifetime constraints with closures and futures
|
||||
info!(" 💡 Authentication check would be done here in a real application");
|
||||
info!(" 💡 Use manager.with_client() to access client methods like whoami()");
|
||||
|
||||
// Demonstrate connection management
|
||||
info!("🔧 Testing connection management...");
|
||||
|
||||
// Disconnect from a specific server
|
||||
info!(" 🔌 Disconnecting from ws://localhost:3031...");
|
||||
if let Err(e) = manager.disconnect_from_server("ws://localhost:3031").await {
|
||||
warn!(" ⚠️ Disconnect error: {}", e);
|
||||
} else {
|
||||
info!(" ✅ Disconnected successfully");
|
||||
}
|
||||
|
||||
// Check status after disconnect
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
let new_statuses = manager.get_all_connection_statuses();
|
||||
for (url, status) in &new_statuses {
|
||||
info!(" {} -> {}", url, status);
|
||||
}
|
||||
|
||||
// Reconnect
|
||||
info!(" 🔌 Reconnecting to ws://localhost:3031...");
|
||||
if let Err(e) = manager.connect_to_server("ws://localhost:3031").await {
|
||||
warn!(" ⚠️ Reconnect error: {}", e);
|
||||
} else {
|
||||
info!(" ✅ Reconnected successfully");
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
// Final status check
|
||||
info!("📊 Final connection status:");
|
||||
let final_statuses = manager.get_all_connection_statuses();
|
||||
for (url, status) in &final_statuses {
|
||||
info!(" {} -> {}", url, status);
|
||||
}
|
||||
|
||||
// Demonstrate adding a connection at runtime
|
||||
info!("➕ Adding new connection at runtime...");
|
||||
manager.add_connection(
|
||||
"ws://localhost:3032".to_string(),
|
||||
Some(private_key.to_string())
|
||||
);
|
||||
|
||||
// Try to connect to the new server (will fail if server isn't running)
|
||||
if let Err(e) = manager.connect_to_server("ws://localhost:3032").await {
|
||||
warn!(" ⚠️ Could not connect to ws://localhost:3032: {}", e);
|
||||
warn!(" 💡 Start a server on port 3032 to test this feature");
|
||||
} else {
|
||||
info!(" ✅ Connected to new server ws://localhost:3032");
|
||||
}
|
||||
|
||||
info!("🎉 WebSocket Manager Demo completed!");
|
||||
info!("💡 Key takeaways:");
|
||||
info!(" - Use WsManager::builder() to configure multiple servers");
|
||||
info!(" - Call connect() to establish all connections");
|
||||
info!(" - Use with_client() for operations on specific servers");
|
||||
info!(" - Use with_all_clients() for bulk operations");
|
||||
info!(" - Connections are managed automatically with keep-alive");
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
pub mod toast;
|
||||
|
||||
pub use toast::*;
|
57
src/lib.rs
57
src/lib.rs
@ -1,57 +0,0 @@
|
||||
//! Framework WebSocket Connection Manager
|
||||
//!
|
||||
//! A generic WebSocket connection manager library that provides:
|
||||
//! - Multiple persistent WebSocket connections
|
||||
//! - secp256k1 authentication support
|
||||
//! - Rhai script execution via the `play` function
|
||||
//! - Cross-platform support (WASM + Native)
|
||||
//! - Generic data type handling
|
||||
|
||||
pub mod ws_manager;
|
||||
pub mod auth;
|
||||
pub mod error;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod browser_auth;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod components;
|
||||
|
||||
// Re-export main types for easy access
|
||||
pub use ws_manager::{WsManager, WsManagerBuilder, WsConnectionManager, ws_manager};
|
||||
pub use auth::AuthConfig;
|
||||
pub use error::{WsError, WsResult};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use browser_auth::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use components::*;
|
||||
|
||||
// Re-export circle_client_ws types that users might need
|
||||
pub use circle_client_ws::{
|
||||
CircleWsClient, CircleWsClientBuilder, CircleWsClientError, PlayResultClient
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use yew::Callback;
|
||||
|
||||
/// Version information
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Prelude module for convenient imports
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
WsManager, WsManagerBuilder, WsConnectionManager, ws_manager,
|
||||
AuthConfig, WsError, WsResult, PlayResultClient
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use yew::Callback;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use crate::browser_auth::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use crate::components::*;
|
||||
}
|
BIN
widgets/.DS_Store
vendored
Normal file
BIN
widgets/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
widgets/file_browser_widget/.DS_Store
vendored
Normal file
BIN
widgets/file_browser_widget/.DS_Store
vendored
Normal file
Binary file not shown.
2905
widgets/file_browser_widget/Cargo.lock
generated
Normal file
2905
widgets/file_browser_widget/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
widgets/file_browser_widget/Cargo.toml
Normal file
45
widgets/file_browser_widget/Cargo.toml
Normal file
@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "file_browser_widget"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "WASM widget for embedding the FileBrowser component in web applications"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/herocode/framework"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
|
||||
|
||||
[dependencies]
|
||||
components = { path = "../../components" }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
yew = { version = "0.21", features = ["csr"] }
|
||||
gloo = "0.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.6"
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"console",
|
||||
"Document",
|
||||
"Element",
|
||||
"HtmlElement",
|
||||
"Window",
|
||||
"Location",
|
||||
"UrlSearchParams"
|
||||
]
|
||||
|
||||
[[example]]
|
||||
name = "server"
|
||||
path = "examples/main.rs"
|
||||
|
||||
[profile.release]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
opt-level = "s"
|
||||
# Enable link time optimization
|
||||
lto = true
|
331
widgets/file_browser_widget/README.md
Normal file
331
widgets/file_browser_widget/README.md
Normal file
@ -0,0 +1,331 @@
|
||||
# FileBrowser Widget
|
||||
|
||||
A powerful WebAssembly-based file browser widget that can be embedded in any web application. Built with Rust and Yew, compiled to WASM for maximum performance.
|
||||
|
||||
## Features
|
||||
|
||||
- 📁 **File & Directory Management**: Browse, create, delete files and directories
|
||||
- 📤 **Resumable Uploads**: Upload files with progress tracking using TUS protocol
|
||||
- 📥 **File Downloads**: Download files with proper MIME type handling
|
||||
- ✏️ **File Editing**: Built-in editors for text files and markdown with live preview
|
||||
- 🎨 **Modern UI**: Responsive Bootstrap-based interface with light/dark themes
|
||||
- 🔧 **Highly Configurable**: Extensive JavaScript API for customization
|
||||
- 🚀 **High Performance**: Compiled to WebAssembly for native-like performance
|
||||
- 🌐 **Cross-Browser**: Works in all modern browsers
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Installation
|
||||
|
||||
#### Via npm (recommended)
|
||||
```bash
|
||||
npm install @herocode/file-browser-widget
|
||||
```
|
||||
|
||||
#### Manual Download
|
||||
Download the latest release files:
|
||||
- `file_browser_widget.js`
|
||||
- `file_browser_widget_bg.wasm`
|
||||
|
||||
### 2. Include Dependencies
|
||||
|
||||
Add Bootstrap CSS and Icons to your HTML:
|
||||
```html
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
```
|
||||
|
||||
### 3. Create Container
|
||||
|
||||
Add a container element to your HTML:
|
||||
```html
|
||||
<div id="file-browser-container"></div>
|
||||
```
|
||||
|
||||
### 4. Initialize Widget
|
||||
|
||||
```javascript
|
||||
import init, {
|
||||
create_file_browser_widget,
|
||||
create_default_config
|
||||
} from '@herocode/file-browser-widget';
|
||||
|
||||
async function initFileBrowser() {
|
||||
// Initialize the WASM module
|
||||
await init();
|
||||
|
||||
// Create configuration
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
config.set_theme('light');
|
||||
config.set_show_upload(true);
|
||||
config.set_show_download(true);
|
||||
config.set_show_delete(true);
|
||||
config.set_max_file_size(100 * 1024 * 1024); // 100MB
|
||||
|
||||
// Create and mount the widget
|
||||
const widget = create_file_browser_widget('file-browser-container', config);
|
||||
|
||||
console.log('FileBrowser widget initialized successfully!');
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
initFileBrowser().catch(console.error);
|
||||
```
|
||||
|
||||
## Configuration API
|
||||
|
||||
### WidgetConfig
|
||||
|
||||
The main configuration object for customizing the widget behavior:
|
||||
|
||||
```javascript
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
|
||||
// File size limit (in bytes)
|
||||
config.set_max_file_size(50 * 1024 * 1024); // 50MB
|
||||
|
||||
// Feature toggles
|
||||
config.set_show_upload(true);
|
||||
config.set_show_download(true);
|
||||
config.set_show_delete(true);
|
||||
|
||||
// UI customization
|
||||
config.set_theme('dark'); // 'light' or 'dark'
|
||||
config.set_css_classes('my-custom-class');
|
||||
config.set_initial_path('documents/');
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `base_endpoint` | string | required | Backend API endpoint for file operations |
|
||||
| `max_file_size` | number | 104857600 | Maximum file size for uploads (bytes) |
|
||||
| `show_upload` | boolean | true | Enable/disable upload functionality |
|
||||
| `show_download` | boolean | true | Enable/disable download functionality |
|
||||
| `show_delete` | boolean | true | Enable/disable delete functionality |
|
||||
| `theme` | string | 'light' | UI theme ('light' or 'dark') |
|
||||
| `css_classes` | string | '' | Additional CSS classes for root element |
|
||||
| `initial_path` | string | '' | Initial directory path to display |
|
||||
|
||||
## JavaScript API
|
||||
|
||||
### Functions
|
||||
|
||||
#### `init()`
|
||||
Initialize the WASM module. Must be called before using other functions.
|
||||
|
||||
```javascript
|
||||
await init();
|
||||
```
|
||||
|
||||
#### `create_default_config(base_endpoint)`
|
||||
Create a default configuration object.
|
||||
|
||||
```javascript
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
```
|
||||
|
||||
#### `create_file_browser_widget(container_id, config)`
|
||||
Create and mount a widget to a DOM element by ID.
|
||||
|
||||
```javascript
|
||||
const widget = create_file_browser_widget('my-container', config);
|
||||
```
|
||||
|
||||
#### `create_file_browser_widget_on_element(element, config)`
|
||||
Create and mount a widget to a specific DOM element.
|
||||
|
||||
```javascript
|
||||
const element = document.getElementById('my-container');
|
||||
const widget = create_file_browser_widget_on_element(element, config);
|
||||
```
|
||||
|
||||
#### `check_browser_compatibility()`
|
||||
Check if the current browser supports the widget.
|
||||
|
||||
```javascript
|
||||
if (!check_browser_compatibility()) {
|
||||
console.error('Browser not supported');
|
||||
}
|
||||
```
|
||||
|
||||
#### `get_version()`
|
||||
Get the widget version.
|
||||
|
||||
```javascript
|
||||
console.log('Widget version:', get_version());
|
||||
```
|
||||
|
||||
### Widget Handle
|
||||
|
||||
The widget functions return a handle that can be used to manage the widget:
|
||||
|
||||
```javascript
|
||||
const widget = create_file_browser_widget('container', config);
|
||||
|
||||
// Destroy the widget
|
||||
widget.destroy();
|
||||
```
|
||||
|
||||
## Backend Requirements
|
||||
|
||||
The widget expects a REST API compatible with the following endpoints:
|
||||
|
||||
### File Listing
|
||||
```
|
||||
GET /files/list/<path>
|
||||
Response: JSON array of file/directory objects
|
||||
```
|
||||
|
||||
### Directory Creation
|
||||
```
|
||||
POST /files/dirs/<path>
|
||||
Creates a new directory at the specified path
|
||||
```
|
||||
|
||||
### File/Directory Deletion
|
||||
```
|
||||
DELETE /files/delete/<path>
|
||||
Deletes the file or directory at the specified path
|
||||
```
|
||||
|
||||
### File Upload (TUS Protocol)
|
||||
```
|
||||
POST /files/upload
|
||||
Handles resumable file uploads using TUS protocol
|
||||
```
|
||||
|
||||
### File Download
|
||||
```
|
||||
GET /files/download/<path>
|
||||
Returns the file content with appropriate headers
|
||||
```
|
||||
|
||||
### Example Backend Response Format
|
||||
|
||||
File listing response:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "document.pdf",
|
||||
"path": "documents/document.pdf",
|
||||
"size": 1024000,
|
||||
"is_directory": false,
|
||||
"modified": "2023-12-01T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"name": "images",
|
||||
"path": "documents/images",
|
||||
"size": 0,
|
||||
"is_directory": true,
|
||||
"modified": "2023-12-01T09:15:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Styling
|
||||
|
||||
Add custom CSS to style the widget:
|
||||
|
||||
```css
|
||||
.file-browser-widget {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-browser-widget .card {
|
||||
border: none;
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Widgets
|
||||
|
||||
You can create multiple widget instances on the same page:
|
||||
|
||||
```javascript
|
||||
// Widget 1
|
||||
const config1 = create_default_config('http://api1.example.com/files');
|
||||
const widget1 = create_file_browser_widget('container1', config1);
|
||||
|
||||
// Widget 2
|
||||
const config2 = create_default_config('http://api2.example.com/files');
|
||||
config2.set_theme('dark');
|
||||
const widget2 = create_file_browser_widget('container2', config2);
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const widget = create_file_browser_widget('container', config);
|
||||
} catch (error) {
|
||||
console.error('Failed to create widget:', error);
|
||||
// Show fallback UI
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Building from Source
|
||||
|
||||
1. Install Rust and wasm-pack:
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
cargo install wasm-pack
|
||||
```
|
||||
|
||||
2. Build the widget:
|
||||
```bash
|
||||
./build.sh
|
||||
```
|
||||
|
||||
3. Test locally:
|
||||
```bash
|
||||
npm run dev
|
||||
# Open http://localhost:8000/example.html
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
widgets/file_browser_widget/
|
||||
├── src/
|
||||
│ └── lib.rs # Main widget implementation
|
||||
├── dist/ # Built distribution files
|
||||
├── pkg/ # wasm-pack output
|
||||
├── Cargo.toml # Rust dependencies
|
||||
├── package.json # npm package config
|
||||
├── build.sh # Build script
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Chrome 57+
|
||||
- Firefox 52+
|
||||
- Safari 11+
|
||||
- Edge 16+
|
||||
|
||||
The widget requires WebAssembly support and modern JavaScript features.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests if applicable
|
||||
5. Submit a pull request
|
||||
|
||||
## Support
|
||||
|
||||
- 📖 [Documentation](https://github.com/herocode/framework)
|
||||
- 🐛 [Issue Tracker](https://github.com/herocode/framework/issues)
|
||||
- 💬 [Discussions](https://github.com/herocode/framework/discussions)
|
49
widgets/file_browser_widget/build.sh
Executable file
49
widgets/file_browser_widget/build.sh
Executable file
@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for FileBrowser Widget
|
||||
# This script creates a unified distribution in dist/ that works for both:
|
||||
# 1. NPM distribution (includes package.json, TypeScript definitions, README)
|
||||
# 2. Direct embedding (includes bundled Uppy.js dependencies)
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 Building FileBrowser Widget..."
|
||||
|
||||
# Clean previous builds
|
||||
echo "🧹 Cleaning previous builds..."
|
||||
rm -rf dist/
|
||||
|
||||
# Build with wasm-pack directly to dist directory
|
||||
echo "📦 Building WASM package directly to dist/..."
|
||||
wasm-pack build --target web --out-dir dist --release
|
||||
|
||||
# Download and bundle Uppy.js dependencies for self-contained use
|
||||
echo "📦 Downloading Uppy.js dependencies..."
|
||||
curl -s "https://releases.transloadit.com/uppy/v3.25.4/uppy.min.js" -o dist/uppy.min.js
|
||||
curl -s "https://releases.transloadit.com/uppy/v3.25.4/uppy.min.css" -o dist/uppy.min.css
|
||||
|
||||
# Verify files were downloaded correctly
|
||||
if [ -s dist/uppy.min.js ] && [ -s dist/uppy.min.css ]; then
|
||||
echo "✅ Uppy.js bundled successfully ($(wc -c < dist/uppy.min.js) bytes JS, $(wc -c < dist/uppy.min.css) bytes CSS)"
|
||||
else
|
||||
echo "❌ Failed to download Uppy.js files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Build complete!"
|
||||
echo ""
|
||||
echo "📦 Unified distribution created in dist/ with:"
|
||||
echo " 🔹 NPM support: package.json, TypeScript definitions, README"
|
||||
echo " 🔹 Direct embedding: bundled Uppy.js dependencies"
|
||||
echo " 🔹 Self-contained: no external dependencies required"
|
||||
echo ""
|
||||
echo "🌐 Use 'cargo run --example server' to test the widget"
|
||||
echo "📦 Use 'npm publish dist/' to publish to npm"
|
||||
echo ""
|
||||
|
||||
echo "🎯 All widget files ready in dist/ directory:"
|
||||
echo " - file_browser_widget.js ($(wc -c < dist/file_browser_widget.js 2>/dev/null || echo '0') bytes)"
|
||||
echo " - file_browser_widget_bg.wasm ($(wc -c < dist/file_browser_widget_bg.wasm 2>/dev/null || echo '0') bytes)"
|
||||
echo " - uppy.min.js ($(wc -c < dist/uppy.min.js 2>/dev/null || echo '0') bytes)"
|
||||
echo " - uppy.min.css ($(wc -c < dist/uppy.min.css 2>/dev/null || echo '0') bytes)"
|
||||
echo " - package.json, *.d.ts, README.md (npm metadata)"
|
208
widgets/file_browser_widget/examples/README.md
Normal file
208
widgets/file_browser_widget/examples/README.md
Normal file
@ -0,0 +1,208 @@
|
||||
# FileBrowser Widget
|
||||
|
||||
A WebAssembly-based file browser widget that can be embedded in any web application.
|
||||
|
||||
## Features
|
||||
|
||||
- File and directory browsing
|
||||
- File upload with progress tracking (using TUS protocol)
|
||||
- File download
|
||||
- Directory creation and deletion
|
||||
- File editing (markdown with live preview, text files)
|
||||
|
||||
## Running the Example
|
||||
|
||||
1. **Start a local server** (required for WASM):
|
||||
```bash
|
||||
python3 -m http.server 8081
|
||||
# or
|
||||
npx serve .
|
||||
```
|
||||
|
||||
2. **Start the mock backend** (in another terminal):
|
||||
```bash
|
||||
cd ../file_browser_demo
|
||||
cargo run --bin mock_server
|
||||
```
|
||||
|
||||
3. **Open the example**:
|
||||
- Navigate to `http://localhost:8081`
|
||||
- The widget will load with a configuration panel
|
||||
- Try different settings and see them applied in real-time
|
||||
|
||||
## Key Features Demonstrated
|
||||
|
||||
### Runtime Configuration
|
||||
The example shows how to configure the widget at runtime without rebuilding:
|
||||
|
||||
```javascript
|
||||
// Create base configuration
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
|
||||
// Apply runtime settings using corrected method names
|
||||
config.setTheme('light'); // Theme selection
|
||||
config.setMaxFileSize(100 * 1024 * 1024); // 100MB limit
|
||||
config.setShowUpload(true); // Enable upload
|
||||
config.setShowDownload(true); // Enable download
|
||||
config.setShowDelete(false); // Disable delete
|
||||
config.setInitialPath('documents/'); // Start in documents folder
|
||||
|
||||
// Create widget with configuration
|
||||
const widget = create_file_browser_widget('container-id', config);
|
||||
```
|
||||
|
||||
### Dynamic Reconfiguration
|
||||
The widget can be recreated with new settings:
|
||||
|
||||
```javascript
|
||||
function updateWidget() {
|
||||
// Destroy existing widget
|
||||
if (currentWidget) {
|
||||
currentWidget.destroy();
|
||||
}
|
||||
|
||||
// Create new widget with updated config
|
||||
const newConfig = create_default_config(newEndpoint);
|
||||
newConfig.setTheme(selectedTheme);
|
||||
currentWidget = create_file_browser_widget('container', newConfig);
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
The example includes comprehensive error handling:
|
||||
|
||||
- WASM initialization errors
|
||||
- Browser compatibility checks
|
||||
- Widget creation failures
|
||||
- Network connectivity issues
|
||||
|
||||
## Widget API Reference
|
||||
|
||||
### Core Functions
|
||||
|
||||
```javascript
|
||||
// Initialize WASM module (call once)
|
||||
await init();
|
||||
|
||||
// Create default configuration
|
||||
const config = create_default_config(baseEndpoint);
|
||||
|
||||
// Create widget instance
|
||||
const widget = create_file_browser_widget(containerId, config);
|
||||
|
||||
// Utility functions
|
||||
const version = get_version();
|
||||
const isCompatible = check_browser_compatibility();
|
||||
```
|
||||
|
||||
### Configuration Methods
|
||||
|
||||
```javascript
|
||||
config.setTheme(theme); // 'light' | 'dark'
|
||||
config.setMaxFileSize(bytes); // Number in bytes
|
||||
config.setShowUpload(enabled); // Boolean
|
||||
config.setShowDownload(enabled); // Boolean
|
||||
config.setShowDelete(enabled); // Boolean
|
||||
config.setCssClasses(classes); // String of CSS classes
|
||||
config.setInitialPath(path); // String path
|
||||
```
|
||||
|
||||
### Widget Handle Methods
|
||||
|
||||
```javascript
|
||||
widget.destroy(); // Clean up widget
|
||||
// Note: Currently no update method - recreate widget for config changes
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Styling
|
||||
```javascript
|
||||
config.setCssClasses('my-custom-theme dark-mode');
|
||||
```
|
||||
|
||||
### Multiple Widgets
|
||||
```javascript
|
||||
const widget1 = create_file_browser_widget('container1', config1);
|
||||
const widget2 = create_file_browser_widget('container2', config2);
|
||||
```
|
||||
|
||||
### Integration with Frameworks
|
||||
|
||||
**React:**
|
||||
```jsx
|
||||
function FileBrowserComponent({ endpoint }) {
|
||||
const containerRef = useRef();
|
||||
const widgetRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
async function initWidget() {
|
||||
await init();
|
||||
const config = create_default_config(endpoint);
|
||||
widgetRef.current = create_file_browser_widget(
|
||||
containerRef.current,
|
||||
config
|
||||
);
|
||||
}
|
||||
initWidget();
|
||||
|
||||
return () => widgetRef.current?.destroy();
|
||||
}, [endpoint]);
|
||||
|
||||
return <div ref={containerRef} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Vue:**
|
||||
```vue
|
||||
<template>
|
||||
<div ref="container"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async mounted() {
|
||||
await init();
|
||||
const config = create_default_config(this.endpoint);
|
||||
this.widget = create_file_browser_widget(this.$refs.container, config);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.widget?.destroy();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"config.setTheme is not a function"**
|
||||
- Ensure you're using the latest widget build
|
||||
- Check that WASM module is properly initialized
|
||||
|
||||
2. **Widget not appearing**
|
||||
- Verify container element exists
|
||||
- Check browser console for errors
|
||||
- Ensure WASM files are served correctly
|
||||
|
||||
3. **Backend connection errors**
|
||||
- Verify backend is running on specified endpoint
|
||||
- Check CORS configuration
|
||||
- Ensure all required API endpoints are implemented
|
||||
|
||||
### Debug Mode
|
||||
```javascript
|
||||
// Enable debug logging
|
||||
console.log('Widget version:', get_version());
|
||||
console.log('Browser compatible:', check_browser_compatibility());
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- **Initial Load**: ~368KB total (WASM + JS)
|
||||
- **Runtime Memory**: ~2-5MB depending on file list size
|
||||
- **Startup Time**: ~100-300ms on modern browsers
|
||||
- **File Operations**: Near-native performance via WASM
|
||||
|
||||
The widget is optimized for production use with minimal overhead.
|
Binary file not shown.
Binary file not shown.
BIN
widgets/file_browser_widget/examples/compressed/uppy.min.css.gz
Normal file
BIN
widgets/file_browser_widget/examples/compressed/uppy.min.css.gz
Normal file
Binary file not shown.
BIN
widgets/file_browser_widget/examples/compressed/uppy.min.js.gz
Normal file
BIN
widgets/file_browser_widget/examples/compressed/uppy.min.js.gz
Normal file
Binary file not shown.
412
widgets/file_browser_widget/examples/index.html
Normal file
412
widgets/file_browser_widget/examples/index.html
Normal file
@ -0,0 +1,412 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FileBrowser Widget Example</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<script src="/uppy.min.js"></script>
|
||||
<link href="/uppy.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.widget-container {
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
min-height: 400px;
|
||||
}
|
||||
.widget-container:empty::after {
|
||||
content: "Widget will render here...";
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
}
|
||||
.config-panel {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.status-success { background-color: #28a745; }
|
||||
.status-error { background-color: #dc3545; }
|
||||
.status-loading { background-color: #ffc107; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<!-- Widget Header -->
|
||||
<div class="config-panel mb-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<h3 class="mb-0">
|
||||
<i class="bi bi-hdd-stack text-primary"></i>
|
||||
File Browser Widget
|
||||
<span class="badge bg-secondary ms-2" id="widget-version">v0.1.0</span>
|
||||
</h3>
|
||||
<p class="text-muted mb-0 mt-1">Self-contained WASM widget for file management</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#assetsModal">
|
||||
<i class="bi bi-file-earmark-zip"></i>
|
||||
Assets
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="window.open('https://github.com/herocode/framework/tree/main/widgets/file_browser_widget', '_blank')">
|
||||
<i class="bi bi-code-slash"></i>
|
||||
Code
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="window.open('#documentation', '_blank')">
|
||||
<i class="bi bi-book"></i>
|
||||
Documentation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="config-panel">
|
||||
<h4>
|
||||
<i class="bi bi-gear"></i>
|
||||
Configuration
|
||||
</h4>
|
||||
<div class="mb-3">
|
||||
<label for="endpoint" class="form-label">Base Endpoint:</label>
|
||||
<input type="text" id="endpoint" class="form-control" value="http://localhost:3001/files">
|
||||
<div class="form-text">Backend API endpoint for file operations</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="theme" class="form-label">Theme:</label>
|
||||
<select id="theme" class="form-select">
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="max-file-size" class="form-label">Max File Size (MB):</label>
|
||||
<input type="number" id="max-file-size" class="form-control" value="100" min="1" max="1000">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Features:</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="show-upload" checked>
|
||||
<label class="form-check-label" for="show-upload">Show Upload</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="show-download" checked>
|
||||
<label class="form-check-label" for="show-download">Show Download</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="show-delete" checked>
|
||||
<label class="form-check-label" for="show-delete">Show Delete</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="initial-path" class="form-label">Initial Path:</label>
|
||||
<input type="text" id="initial-path" class="form-control" placeholder="e.g., documents/">
|
||||
</div>
|
||||
|
||||
<button id="recreate-widget" class="btn btn-primary w-100">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
Apply Configuration
|
||||
</button>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div id="status" class="small">
|
||||
<span class="status-indicator status-loading"></span>
|
||||
<span id="status-text">Initializing...</span>
|
||||
</div>
|
||||
<div class="small">
|
||||
<span class="badge bg-success" id="browser-compat">Compatible</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<!-- Widget Rendering Area -->
|
||||
<div class="widget-container">
|
||||
<div id="file-browser-widget"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import init, {
|
||||
create_file_browser_widget,
|
||||
create_default_config,
|
||||
check_browser_compatibility,
|
||||
get_version
|
||||
} from '/file_browser_widget.js';
|
||||
|
||||
let currentWidget = null;
|
||||
let isInitialized = false;
|
||||
|
||||
function updateStatus(text, type = 'loading') {
|
||||
const statusElement = document.getElementById('status-text');
|
||||
const indicatorElement = document.querySelector('.status-indicator');
|
||||
|
||||
statusElement.textContent = text;
|
||||
indicatorElement.className = `status-indicator status-${type}`;
|
||||
}
|
||||
|
||||
async function initWidget() {
|
||||
try {
|
||||
updateStatus('Loading WASM module...', 'loading');
|
||||
await init();
|
||||
|
||||
updateStatus('Checking compatibility...', 'loading');
|
||||
const version = get_version();
|
||||
const isCompatible = check_browser_compatibility();
|
||||
|
||||
document.getElementById('widget-version').textContent = version;
|
||||
document.getElementById('browser-compat').textContent = isCompatible ? 'Yes ✓' : 'No ✗';
|
||||
|
||||
if (!isCompatible) {
|
||||
updateStatus('Browser not compatible', 'error');
|
||||
document.getElementById('file-browser-widget').innerHTML =
|
||||
'<div class="alert alert-danger">Your browser is not compatible with this widget</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
updateStatus('Ready', 'success');
|
||||
createWidget();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize widget:', error);
|
||||
updateStatus(`Initialization failed: ${error.message}`, 'error');
|
||||
document.getElementById('file-browser-widget').innerHTML =
|
||||
`<div class="alert alert-danger">Failed to initialize: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function createWidget() {
|
||||
if (!isInitialized) {
|
||||
updateStatus('Widget not initialized', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
updateStatus('Creating widget...', 'loading');
|
||||
|
||||
// Destroy existing widget
|
||||
if (currentWidget) {
|
||||
currentWidget.destroy();
|
||||
currentWidget = null;
|
||||
}
|
||||
|
||||
// Clear container
|
||||
const container = document.getElementById('file-browser-widget');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Get configuration from form
|
||||
const config = create_default_config(document.getElementById('endpoint').value);
|
||||
|
||||
// Apply configuration using the corrected method names
|
||||
config.setTheme(document.getElementById('theme').value);
|
||||
config.setMaxFileSize(parseInt(document.getElementById('max-file-size').value) * 1024 * 1024);
|
||||
config.setShowUpload(document.getElementById('show-upload').checked);
|
||||
config.setShowDownload(document.getElementById('show-download').checked);
|
||||
config.setShowDelete(document.getElementById('show-delete').checked);
|
||||
|
||||
const initialPath = document.getElementById('initial-path').value.trim();
|
||||
if (initialPath) {
|
||||
config.setInitialPath(initialPath);
|
||||
}
|
||||
|
||||
// Create widget
|
||||
currentWidget = create_file_browser_widget('file-browser-widget', config);
|
||||
updateStatus('Widget ready', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create widget:', error);
|
||||
updateStatus(`Widget creation failed: ${error.message}`, 'error');
|
||||
document.getElementById('file-browser-widget').innerHTML =
|
||||
`<div class="alert alert-danger">Failed to create widget: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('recreate-widget').addEventListener('click', createWidget);
|
||||
|
||||
// Auto-recreate on configuration changes
|
||||
['endpoint', 'theme', 'max-file-size', 'show-upload', 'show-download', 'show-delete', 'initial-path'].forEach(id => {
|
||||
const element = document.getElementById(id);
|
||||
if (element.type === 'checkbox') {
|
||||
element.addEventListener('change', () => {
|
||||
if (isInitialized) createWidget();
|
||||
});
|
||||
} else {
|
||||
element.addEventListener('input', () => {
|
||||
if (isInitialized) {
|
||||
clearTimeout(element.debounceTimer);
|
||||
element.debounceTimer = setTimeout(createWidget, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize when page loads
|
||||
initWidget();
|
||||
</script>
|
||||
|
||||
<!-- Assets Modal -->
|
||||
<div class="modal fade" id="assetsModal" tabindex="-1" aria-labelledby="assetsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="assetsModalLabel">
|
||||
<i class="bi bi-file-earmark-zip text-primary"></i>
|
||||
Widget Assets & Size Optimization
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-success">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
Distribution Files
|
||||
</h6>
|
||||
<p class="small text-muted mb-3">Self-contained widget distribution with no external dependencies.</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<div class="small">
|
||||
<div class="badge bg-success mb-2">67.9% compression ratio</div>
|
||||
<div class="text-muted">Optimized for web delivery</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Asset</th>
|
||||
<th>Description</th>
|
||||
<th class="text-end">Original</th>
|
||||
<th class="text-end">Gzipped</th>
|
||||
<th class="text-end">Savings</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_browser_widget_bg.wasm</code></td>
|
||||
<td class="small text-muted">WebAssembly binary</td>
|
||||
<td class="text-end">331KB</td>
|
||||
<td class="text-end text-info">136KB</td>
|
||||
<td class="text-end text-success">59%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_browser_widget.js</code></td>
|
||||
<td class="small text-muted">JavaScript bindings</td>
|
||||
<td class="text-end">39KB</td>
|
||||
<td class="text-end text-info">7KB</td>
|
||||
<td class="text-end text-success">82%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>uppy.min.js</code></td>
|
||||
<td class="small text-muted">File upload library</td>
|
||||
<td class="text-end">564KB</td>
|
||||
<td class="text-end text-info">172KB</td>
|
||||
<td class="text-end text-success">70%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>uppy.min.css</code></td>
|
||||
<td class="small text-muted">Upload UI styling</td>
|
||||
<td class="text-end">90KB</td>
|
||||
<td class="text-end text-info">14KB</td>
|
||||
<td class="text-end text-success">84%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_browser_widget.d.ts</code></td>
|
||||
<td class="small text-muted">TypeScript definitions</td>
|
||||
<td class="text-end">5KB</td>
|
||||
<td class="text-end text-muted">-</td>
|
||||
<td class="text-end text-muted">-</td>
|
||||
</tr>
|
||||
<tr class="table-active fw-bold">
|
||||
<td>Total Distribution</td>
|
||||
<td class="small text-muted">Complete widget package</td>
|
||||
<td class="text-end">1.03MB</td>
|
||||
<td class="text-end text-success">329KB</td>
|
||||
<td class="text-end text-success">68%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-info">
|
||||
<i class="bi bi-speedometer2"></i>
|
||||
Performance Benefits
|
||||
</h6>
|
||||
<ul class="small">
|
||||
<li>Faster initial load times</li>
|
||||
<li>Reduced bandwidth usage</li>
|
||||
<li>Better mobile experience</li>
|
||||
<li>CDN-friendly distribution</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-warning">
|
||||
<i class="bi bi-tools"></i>
|
||||
Optimization Techniques
|
||||
</h6>
|
||||
<ul class="small">
|
||||
<li>wasm-opt binary optimization</li>
|
||||
<li>Gzip compression (level 9)</li>
|
||||
<li>Dead code elimination</li>
|
||||
<li>Release build optimizations</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" onclick="window.open('https://github.com/herocode/framework/tree/main/widgets/file_browser_widget', '_blank')">
|
||||
<i class="bi bi-download"></i>
|
||||
Download Widget
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
237
widgets/file_browser_widget/examples/main.rs
Normal file
237
widgets/file_browser_widget/examples/main.rs
Normal file
@ -0,0 +1,237 @@
|
||||
use std::fs;
|
||||
use std::io::prelude::*;
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
println!("🚀 Starting FileBrowser Widget Example Server...");
|
||||
println!();
|
||||
|
||||
// Check if we have the built widget files in dist/ directory
|
||||
let dist_dir = Path::new("dist");
|
||||
let widget_files = [
|
||||
"file_browser_widget.js",
|
||||
"file_browser_widget_bg.wasm",
|
||||
"uppy.min.js",
|
||||
"uppy.min.css"
|
||||
];
|
||||
|
||||
let mut missing_files = Vec::new();
|
||||
for file in &widget_files {
|
||||
let file_path = dist_dir.join(file);
|
||||
if !file_path.exists() {
|
||||
missing_files.push(*file);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have the HTML file in examples/ directory
|
||||
let examples_dir = Path::new("examples");
|
||||
let html_file = examples_dir.join("index.html");
|
||||
if !html_file.exists() {
|
||||
missing_files.push("examples/index.html");
|
||||
}
|
||||
|
||||
if !missing_files.is_empty() {
|
||||
println!("❌ Error: Missing required files:");
|
||||
for file in &missing_files {
|
||||
println!(" - {}", file);
|
||||
}
|
||||
println!();
|
||||
println!("💡 Run the build script first: ./build.sh");
|
||||
println!(" This will generate the required widget files in dist/.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!("✅ All required files found");
|
||||
println!();
|
||||
|
||||
// Create compressed versions for optimized serving
|
||||
create_compressed_assets();
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:8081").unwrap();
|
||||
println!("🌐 FileBrowser Widget Example Server");
|
||||
println!("📡 Serving on http://localhost:8081");
|
||||
println!("🔗 Open http://localhost:8081 in your browser to test the widget");
|
||||
println!("⏹️ Press Ctrl+C to stop the server");
|
||||
println!();
|
||||
|
||||
for stream in listener.incoming() {
|
||||
let stream = stream.unwrap();
|
||||
handle_connection(stream);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_connection(mut stream: TcpStream) {
|
||||
let mut buffer = [0; 1024];
|
||||
stream.read(&mut buffer).unwrap();
|
||||
|
||||
let request = String::from_utf8_lossy(&buffer[..]);
|
||||
let request_line = request.lines().next().unwrap_or("");
|
||||
|
||||
if let Some(path) = request_line.split_whitespace().nth(1) {
|
||||
let file_path = match path {
|
||||
"/" => "index.html", // Serve the HTML file from examples/
|
||||
path if path.starts_with('/') => {
|
||||
let clean_path = &path[1..]; // Remove leading slash
|
||||
|
||||
// Check if this is a request for a static asset
|
||||
if is_static_asset(clean_path) {
|
||||
clean_path
|
||||
} else {
|
||||
// For all non-asset routes, serve index.html to support client-side routing
|
||||
"index.html"
|
||||
}
|
||||
},
|
||||
_ => "index.html",
|
||||
};
|
||||
|
||||
serve_file(&mut stream, file_path);
|
||||
} else {
|
||||
serve_404(&mut stream);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_static_asset(path: &str) -> bool {
|
||||
// Check if the path is for a static asset (widget files)
|
||||
matches!(path,
|
||||
"file_browser_widget.js" |
|
||||
"file_browser_widget_bg.wasm" |
|
||||
"file_browser_widget.d.ts" |
|
||||
"uppy.min.js" |
|
||||
"uppy.min.css" |
|
||||
"favicon.ico"
|
||||
)
|
||||
}
|
||||
|
||||
fn create_compressed_assets() {
|
||||
println!("🗜 Creating compressed assets for optimized serving...");
|
||||
|
||||
// Create examples/compressed directory
|
||||
let compressed_dir = Path::new("examples/compressed");
|
||||
if !compressed_dir.exists() {
|
||||
fs::create_dir_all(compressed_dir).expect("Failed to create compressed directory");
|
||||
}
|
||||
|
||||
// List of files to compress from dist/
|
||||
let files_to_compress = [
|
||||
"file_browser_widget.js",
|
||||
"file_browser_widget_bg.wasm",
|
||||
"uppy.min.js",
|
||||
"uppy.min.css",
|
||||
];
|
||||
|
||||
for file in &files_to_compress {
|
||||
let source_path = format!("dist/{}", file);
|
||||
let compressed_path = format!("examples/compressed/{}.gz", file);
|
||||
|
||||
// Check if source exists and compressed version needs updating
|
||||
if Path::new(&source_path).exists() {
|
||||
let needs_compression = !Path::new(&compressed_path).exists() ||
|
||||
fs::metadata(&source_path).unwrap().modified().unwrap() >
|
||||
fs::metadata(&compressed_path).unwrap_or_else(|_| fs::metadata(&source_path).unwrap()).modified().unwrap();
|
||||
|
||||
if needs_compression {
|
||||
let output = Command::new("gzip")
|
||||
.args(&["-9", "-c", &source_path])
|
||||
.output()
|
||||
.expect("Failed to execute gzip");
|
||||
|
||||
if output.status.success() {
|
||||
fs::write(&compressed_path, output.stdout)
|
||||
.expect("Failed to write compressed file");
|
||||
|
||||
let original_size = fs::metadata(&source_path).unwrap().len();
|
||||
let compressed_size = fs::metadata(&compressed_path).unwrap().len();
|
||||
let ratio = (compressed_size as f64 / original_size as f64 * 100.0) as u32;
|
||||
|
||||
println!(" ✅ {} compressed: {} → {} bytes ({}%)", file, original_size, compressed_size, ratio);
|
||||
} else {
|
||||
println!(" ⚠️ Failed to compress {}", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("🎯 Compressed assets ready in examples/compressed/");
|
||||
println!();
|
||||
}
|
||||
|
||||
fn serve_file(stream: &mut TcpStream, file_path: &str) {
|
||||
let current_dir = std::env::current_dir().unwrap();
|
||||
|
||||
// Determine which directory to serve from based on file type
|
||||
let (full_path, use_gzip) = match file_path {
|
||||
"index.html" => (current_dir.join("examples").join(file_path), false),
|
||||
_ => {
|
||||
let base_path = current_dir.join("dist").join(file_path);
|
||||
let gzip_path = current_dir.join("examples/compressed").join(format!("{}.gz", file_path));
|
||||
|
||||
// Prefer gzipped version from examples/compressed if it exists
|
||||
if gzip_path.exists() {
|
||||
(gzip_path, true)
|
||||
} else {
|
||||
(base_path, false)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if full_path.exists() && full_path.is_file() {
|
||||
match fs::read(&full_path) {
|
||||
Ok(contents) => {
|
||||
let content_type = get_content_type(file_path);
|
||||
let mut response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}",
|
||||
content_type,
|
||||
contents.len()
|
||||
);
|
||||
|
||||
// Add gzip encoding header if serving compressed content
|
||||
if use_gzip {
|
||||
response.push_str("\r\nContent-Encoding: gzip");
|
||||
}
|
||||
|
||||
response.push_str("\r\n\r\n");
|
||||
|
||||
let _ = stream.write_all(response.as_bytes());
|
||||
let _ = stream.write_all(&contents);
|
||||
|
||||
let compression_info = if use_gzip { " (gzipped)" } else { "" };
|
||||
println!("📄 Served: {}{} ({} bytes)", file_path, compression_info, contents.len());
|
||||
}
|
||||
Err(_) => serve_404(stream),
|
||||
}
|
||||
} else {
|
||||
serve_404(stream);
|
||||
}
|
||||
}
|
||||
|
||||
fn serve_404(stream: &mut TcpStream) {
|
||||
let response = "HTTP/1.1 404 NOT FOUND\r\nContent-Type: text/html\r\n\r\n<h1>404 Not Found</h1>";
|
||||
stream.write_all(response.as_bytes()).unwrap();
|
||||
stream.flush().unwrap();
|
||||
println!("❌ 404 Not Found");
|
||||
}
|
||||
|
||||
fn get_content_type(file_path: &str) -> &'static str {
|
||||
let extension = Path::new(file_path)
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
match extension {
|
||||
"html" => "text/html; charset=utf-8",
|
||||
"js" => "application/javascript",
|
||||
"css" => "text/css",
|
||||
"wasm" => "application/wasm",
|
||||
"json" => "application/json",
|
||||
"png" => "image/png",
|
||||
"jpg" | "jpeg" => "image/jpeg",
|
||||
"gif" => "image/gif",
|
||||
"svg" => "image/svg+xml",
|
||||
"ico" => "image/x-icon",
|
||||
"ts" => "application/typescript",
|
||||
"md" => "text/markdown",
|
||||
_ => "text/plain",
|
||||
}
|
||||
}
|
48
widgets/file_browser_widget/package.json
Normal file
48
widgets/file_browser_widget/package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@herocode/file-browser-widget",
|
||||
"version": "0.1.0",
|
||||
"description": "WebAssembly-based file browser widget for web applications",
|
||||
"main": "dist/file_browser_widget.js",
|
||||
"types": "dist/file_browser_widget.d.ts",
|
||||
"files": [
|
||||
"dist/file_browser_widget.js",
|
||||
"dist/file_browser_widget_bg.wasm",
|
||||
"dist/file_browser_widget.d.ts",
|
||||
"dist/README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "./build.sh",
|
||||
"clean": "rm -rf pkg/ dist/",
|
||||
"dev": "python3 -m http.server 8000 --directory dist",
|
||||
"test": "echo \"No tests specified\" && exit 0"
|
||||
},
|
||||
"keywords": [
|
||||
"file-browser",
|
||||
"wasm",
|
||||
"webassembly",
|
||||
"rust",
|
||||
"yew",
|
||||
"widget",
|
||||
"file-upload",
|
||||
"file-manager"
|
||||
],
|
||||
"author": "HeroCode Team",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/herocode/framework.git",
|
||||
"directory": "widgets/file_browser_widget"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/herocode/framework/issues"
|
||||
},
|
||||
"homepage": "https://github.com/herocode/framework#readme",
|
||||
"devDependencies": {},
|
||||
"peerDependencies": {},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
331
widgets/file_browser_widget/pkg/README.md
Normal file
331
widgets/file_browser_widget/pkg/README.md
Normal file
@ -0,0 +1,331 @@
|
||||
# FileBrowser Widget
|
||||
|
||||
A powerful WebAssembly-based file browser widget that can be embedded in any web application. Built with Rust and Yew, compiled to WASM for maximum performance.
|
||||
|
||||
## Features
|
||||
|
||||
- 📁 **File & Directory Management**: Browse, create, delete files and directories
|
||||
- 📤 **Resumable Uploads**: Upload files with progress tracking using TUS protocol
|
||||
- 📥 **File Downloads**: Download files with proper MIME type handling
|
||||
- ✏️ **File Editing**: Built-in editors for text files and markdown with live preview
|
||||
- 🎨 **Modern UI**: Responsive Bootstrap-based interface with light/dark themes
|
||||
- 🔧 **Highly Configurable**: Extensive JavaScript API for customization
|
||||
- 🚀 **High Performance**: Compiled to WebAssembly for native-like performance
|
||||
- 🌐 **Cross-Browser**: Works in all modern browsers
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Installation
|
||||
|
||||
#### Via npm (recommended)
|
||||
```bash
|
||||
npm install @herocode/file-browser-widget
|
||||
```
|
||||
|
||||
#### Manual Download
|
||||
Download the latest release files:
|
||||
- `file_browser_widget.js`
|
||||
- `file_browser_widget_bg.wasm`
|
||||
|
||||
### 2. Include Dependencies
|
||||
|
||||
Add Bootstrap CSS and Icons to your HTML:
|
||||
```html
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
```
|
||||
|
||||
### 3. Create Container
|
||||
|
||||
Add a container element to your HTML:
|
||||
```html
|
||||
<div id="file-browser-container"></div>
|
||||
```
|
||||
|
||||
### 4. Initialize Widget
|
||||
|
||||
```javascript
|
||||
import init, {
|
||||
create_file_browser_widget,
|
||||
create_default_config
|
||||
} from '@herocode/file-browser-widget';
|
||||
|
||||
async function initFileBrowser() {
|
||||
// Initialize the WASM module
|
||||
await init();
|
||||
|
||||
// Create configuration
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
config.set_theme('light');
|
||||
config.set_show_upload(true);
|
||||
config.set_show_download(true);
|
||||
config.set_show_delete(true);
|
||||
config.set_max_file_size(100 * 1024 * 1024); // 100MB
|
||||
|
||||
// Create and mount the widget
|
||||
const widget = create_file_browser_widget('file-browser-container', config);
|
||||
|
||||
console.log('FileBrowser widget initialized successfully!');
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
initFileBrowser().catch(console.error);
|
||||
```
|
||||
|
||||
## Configuration API
|
||||
|
||||
### WidgetConfig
|
||||
|
||||
The main configuration object for customizing the widget behavior:
|
||||
|
||||
```javascript
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
|
||||
// File size limit (in bytes)
|
||||
config.set_max_file_size(50 * 1024 * 1024); // 50MB
|
||||
|
||||
// Feature toggles
|
||||
config.set_show_upload(true);
|
||||
config.set_show_download(true);
|
||||
config.set_show_delete(true);
|
||||
|
||||
// UI customization
|
||||
config.set_theme('dark'); // 'light' or 'dark'
|
||||
config.set_css_classes('my-custom-class');
|
||||
config.set_initial_path('documents/');
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `base_endpoint` | string | required | Backend API endpoint for file operations |
|
||||
| `max_file_size` | number | 104857600 | Maximum file size for uploads (bytes) |
|
||||
| `show_upload` | boolean | true | Enable/disable upload functionality |
|
||||
| `show_download` | boolean | true | Enable/disable download functionality |
|
||||
| `show_delete` | boolean | true | Enable/disable delete functionality |
|
||||
| `theme` | string | 'light' | UI theme ('light' or 'dark') |
|
||||
| `css_classes` | string | '' | Additional CSS classes for root element |
|
||||
| `initial_path` | string | '' | Initial directory path to display |
|
||||
|
||||
## JavaScript API
|
||||
|
||||
### Functions
|
||||
|
||||
#### `init()`
|
||||
Initialize the WASM module. Must be called before using other functions.
|
||||
|
||||
```javascript
|
||||
await init();
|
||||
```
|
||||
|
||||
#### `create_default_config(base_endpoint)`
|
||||
Create a default configuration object.
|
||||
|
||||
```javascript
|
||||
const config = create_default_config('http://localhost:3001/files');
|
||||
```
|
||||
|
||||
#### `create_file_browser_widget(container_id, config)`
|
||||
Create and mount a widget to a DOM element by ID.
|
||||
|
||||
```javascript
|
||||
const widget = create_file_browser_widget('my-container', config);
|
||||
```
|
||||
|
||||
#### `create_file_browser_widget_on_element(element, config)`
|
||||
Create and mount a widget to a specific DOM element.
|
||||
|
||||
```javascript
|
||||
const element = document.getElementById('my-container');
|
||||
const widget = create_file_browser_widget_on_element(element, config);
|
||||
```
|
||||
|
||||
#### `check_browser_compatibility()`
|
||||
Check if the current browser supports the widget.
|
||||
|
||||
```javascript
|
||||
if (!check_browser_compatibility()) {
|
||||
console.error('Browser not supported');
|
||||
}
|
||||
```
|
||||
|
||||
#### `get_version()`
|
||||
Get the widget version.
|
||||
|
||||
```javascript
|
||||
console.log('Widget version:', get_version());
|
||||
```
|
||||
|
||||
### Widget Handle
|
||||
|
||||
The widget functions return a handle that can be used to manage the widget:
|
||||
|
||||
```javascript
|
||||
const widget = create_file_browser_widget('container', config);
|
||||
|
||||
// Destroy the widget
|
||||
widget.destroy();
|
||||
```
|
||||
|
||||
## Backend Requirements
|
||||
|
||||
The widget expects a REST API compatible with the following endpoints:
|
||||
|
||||
### File Listing
|
||||
```
|
||||
GET /files/list/<path>
|
||||
Response: JSON array of file/directory objects
|
||||
```
|
||||
|
||||
### Directory Creation
|
||||
```
|
||||
POST /files/dirs/<path>
|
||||
Creates a new directory at the specified path
|
||||
```
|
||||
|
||||
### File/Directory Deletion
|
||||
```
|
||||
DELETE /files/delete/<path>
|
||||
Deletes the file or directory at the specified path
|
||||
```
|
||||
|
||||
### File Upload (TUS Protocol)
|
||||
```
|
||||
POST /files/upload
|
||||
Handles resumable file uploads using TUS protocol
|
||||
```
|
||||
|
||||
### File Download
|
||||
```
|
||||
GET /files/download/<path>
|
||||
Returns the file content with appropriate headers
|
||||
```
|
||||
|
||||
### Example Backend Response Format
|
||||
|
||||
File listing response:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "document.pdf",
|
||||
"path": "documents/document.pdf",
|
||||
"size": 1024000,
|
||||
"is_directory": false,
|
||||
"modified": "2023-12-01T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"name": "images",
|
||||
"path": "documents/images",
|
||||
"size": 0,
|
||||
"is_directory": true,
|
||||
"modified": "2023-12-01T09:15:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Styling
|
||||
|
||||
Add custom CSS to style the widget:
|
||||
|
||||
```css
|
||||
.file-browser-widget {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-browser-widget .card {
|
||||
border: none;
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Widgets
|
||||
|
||||
You can create multiple widget instances on the same page:
|
||||
|
||||
```javascript
|
||||
// Widget 1
|
||||
const config1 = create_default_config('http://api1.example.com/files');
|
||||
const widget1 = create_file_browser_widget('container1', config1);
|
||||
|
||||
// Widget 2
|
||||
const config2 = create_default_config('http://api2.example.com/files');
|
||||
config2.set_theme('dark');
|
||||
const widget2 = create_file_browser_widget('container2', config2);
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const widget = create_file_browser_widget('container', config);
|
||||
} catch (error) {
|
||||
console.error('Failed to create widget:', error);
|
||||
// Show fallback UI
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Building from Source
|
||||
|
||||
1. Install Rust and wasm-pack:
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
cargo install wasm-pack
|
||||
```
|
||||
|
||||
2. Build the widget:
|
||||
```bash
|
||||
./build.sh
|
||||
```
|
||||
|
||||
3. Test locally:
|
||||
```bash
|
||||
npm run dev
|
||||
# Open http://localhost:8000/example.html
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
widgets/file_browser_widget/
|
||||
├── src/
|
||||
│ └── lib.rs # Main widget implementation
|
||||
├── dist/ # Built distribution files
|
||||
├── pkg/ # wasm-pack output
|
||||
├── Cargo.toml # Rust dependencies
|
||||
├── package.json # npm package config
|
||||
├── build.sh # Build script
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Chrome 57+
|
||||
- Firefox 52+
|
||||
- Safari 11+
|
||||
- Edge 16+
|
||||
|
||||
The widget requires WebAssembly support and modern JavaScript features.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests if applicable
|
||||
5. Submit a pull request
|
||||
|
||||
## Support
|
||||
|
||||
- 📖 [Documentation](https://github.com/herocode/framework)
|
||||
- 🐛 [Issue Tracker](https://github.com/herocode/framework/issues)
|
||||
- 💬 [Discussions](https://github.com/herocode/framework/discussions)
|
111
widgets/file_browser_widget/pkg/file_browser_widget.d.ts
vendored
Normal file
111
widgets/file_browser_widget/pkg/file_browser_widget.d.ts
vendored
Normal file
@ -0,0 +1,111 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export function main(): void;
|
||||
/**
|
||||
* Create and mount a FileBrowser widget to the specified DOM element
|
||||
*/
|
||||
export function create_file_browser_widget(container_id: string, config: JSWidgetConfig): FileBrowserWidgetHandle;
|
||||
/**
|
||||
* Create and mount a FileBrowser widget to a specific DOM element
|
||||
*/
|
||||
export function create_file_browser_widget_on_element(element: Element, config: JSWidgetConfig): FileBrowserWidgetHandle;
|
||||
/**
|
||||
* Utility function to create a default configuration
|
||||
*/
|
||||
export function create_default_config(base_endpoint: string): JSWidgetConfig;
|
||||
/**
|
||||
* Get version information
|
||||
*/
|
||||
export function get_version(): string;
|
||||
/**
|
||||
* Check if the widget is compatible with the current browser
|
||||
*/
|
||||
export function check_browser_compatibility(): boolean;
|
||||
/**
|
||||
* Handle for managing the widget instance
|
||||
*/
|
||||
export class FileBrowserWidgetHandle {
|
||||
private constructor();
|
||||
free(): void;
|
||||
/**
|
||||
* Destroy the widget instance
|
||||
*/
|
||||
destroy(): void;
|
||||
/**
|
||||
* Update the widget configuration
|
||||
*/
|
||||
update_config(_config: JSWidgetConfig): void;
|
||||
}
|
||||
/**
|
||||
* JavaScript-compatible configuration wrapper
|
||||
*/
|
||||
export class JSWidgetConfig {
|
||||
free(): void;
|
||||
constructor(base_endpoint: string);
|
||||
setMaxFileSize(size: number): void;
|
||||
setShowUpload(show: boolean): void;
|
||||
setShowDownload(show: boolean): void;
|
||||
setShowDelete(show: boolean): void;
|
||||
setTheme(theme: string): void;
|
||||
setCssClasses(classes: string): void;
|
||||
setInitialPath(path: string): void;
|
||||
}
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly main: () => void;
|
||||
readonly __wbg_jswidgetconfig_free: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_new: (a: number, b: number) => number;
|
||||
readonly jswidgetconfig_setMaxFileSize: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_setShowUpload: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_setShowDownload: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_setShowDelete: (a: number, b: number) => void;
|
||||
readonly jswidgetconfig_setTheme: (a: number, b: number, c: number) => void;
|
||||
readonly jswidgetconfig_setCssClasses: (a: number, b: number, c: number) => void;
|
||||
readonly jswidgetconfig_setInitialPath: (a: number, b: number, c: number) => void;
|
||||
readonly __wbg_filebrowserwidgethandle_free: (a: number, b: number) => void;
|
||||
readonly filebrowserwidgethandle_destroy: (a: number) => void;
|
||||
readonly filebrowserwidgethandle_update_config: (a: number, b: number) => void;
|
||||
readonly create_file_browser_widget: (a: number, b: number, c: number) => [number, number, number];
|
||||
readonly create_file_browser_widget_on_element: (a: any, b: number) => [number, number, number];
|
||||
readonly create_default_config: (a: number, b: number) => number;
|
||||
readonly get_version: () => [number, number];
|
||||
readonly check_browser_compatibility: () => number;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly __externref_table_alloc: () => number;
|
||||
readonly __wbindgen_export_2: WebAssembly.Table;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __externref_drop_slice: (a: number, b: number) => void;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_export_7: WebAssembly.Table;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly closure26_externref_shim: (a: number, b: number, c: any, d: any) => void;
|
||||
readonly closure23_externref_shim: (a: number, b: number, c: any, d: any, e: any) => void;
|
||||
readonly closure48_externref_shim: (a: number, b: number, c: any) => void;
|
||||
readonly closure58_externref_shim: (a: number, b: number, c: any) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
1074
widgets/file_browser_widget/pkg/file_browser_widget.js
Normal file
1074
widgets/file_browser_widget/pkg/file_browser_widget.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
widgets/file_browser_widget/pkg/file_browser_widget_bg.wasm
Normal file
BIN
widgets/file_browser_widget/pkg/file_browser_widget_bg.wasm
Normal file
Binary file not shown.
35
widgets/file_browser_widget/pkg/file_browser_widget_bg.wasm.d.ts
vendored
Normal file
35
widgets/file_browser_widget/pkg/file_browser_widget_bg.wasm.d.ts
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const main: () => void;
|
||||
export const __wbg_jswidgetconfig_free: (a: number, b: number) => void;
|
||||
export const jswidgetconfig_new: (a: number, b: number) => number;
|
||||
export const jswidgetconfig_setMaxFileSize: (a: number, b: number) => void;
|
||||
export const jswidgetconfig_setShowUpload: (a: number, b: number) => void;
|
||||
export const jswidgetconfig_setShowDownload: (a: number, b: number) => void;
|
||||
export const jswidgetconfig_setShowDelete: (a: number, b: number) => void;
|
||||
export const jswidgetconfig_setTheme: (a: number, b: number, c: number) => void;
|
||||
export const jswidgetconfig_setCssClasses: (a: number, b: number, c: number) => void;
|
||||
export const jswidgetconfig_setInitialPath: (a: number, b: number, c: number) => void;
|
||||
export const __wbg_filebrowserwidgethandle_free: (a: number, b: number) => void;
|
||||
export const filebrowserwidgethandle_destroy: (a: number) => void;
|
||||
export const filebrowserwidgethandle_update_config: (a: number, b: number) => void;
|
||||
export const create_file_browser_widget: (a: number, b: number, c: number) => [number, number, number];
|
||||
export const create_file_browser_widget_on_element: (a: any, b: number) => [number, number, number];
|
||||
export const create_default_config: (a: number, b: number) => number;
|
||||
export const get_version: () => [number, number];
|
||||
export const check_browser_compatibility: () => number;
|
||||
export const __wbindgen_exn_store: (a: number) => void;
|
||||
export const __externref_table_alloc: () => number;
|
||||
export const __wbindgen_export_2: WebAssembly.Table;
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __externref_drop_slice: (a: number, b: number) => void;
|
||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
export const __wbindgen_export_7: WebAssembly.Table;
|
||||
export const __externref_table_dealloc: (a: number) => void;
|
||||
export const closure26_externref_shim: (a: number, b: number, c: any, d: any) => void;
|
||||
export const closure23_externref_shim: (a: number, b: number, c: any, d: any, e: any) => void;
|
||||
export const closure48_externref_shim: (a: number, b: number, c: any) => void;
|
||||
export const closure58_externref_shim: (a: number, b: number, c: any) => void;
|
||||
export const __wbindgen_start: () => void;
|
21
widgets/file_browser_widget/pkg/package.json
Normal file
21
widgets/file_browser_widget/pkg/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "file_browser_widget",
|
||||
"type": "module",
|
||||
"description": "WASM widget for embedding the FileBrowser component in web applications",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/herocode/framework"
|
||||
},
|
||||
"files": [
|
||||
"file_browser_widget_bg.wasm",
|
||||
"file_browser_widget.js",
|
||||
"file_browser_widget.d.ts"
|
||||
],
|
||||
"main": "file_browser_widget.js",
|
||||
"types": "file_browser_widget.d.ts",
|
||||
"sideEffects": [
|
||||
"./snippets/*"
|
||||
]
|
||||
}
|
210
widgets/file_browser_widget/src/lib.rs
Normal file
210
widgets/file_browser_widget/src/lib.rs
Normal file
@ -0,0 +1,210 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use yew::prelude::*;
|
||||
use components::{FileBrowser, FileBrowserConfig};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use web_sys::{Element, HtmlElement};
|
||||
|
||||
// Initialize panic hook for better error messages in development
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
/// Configuration object that can be passed from JavaScript
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Properties)]
|
||||
pub struct WidgetConfig {
|
||||
/// Base endpoint for file operations (e.g., "/files" or "https://api.example.com/files")
|
||||
pub base_endpoint: String,
|
||||
/// Maximum file size for uploads in bytes (default: 100MB)
|
||||
pub max_file_size: Option<u64>,
|
||||
/// Whether to show upload functionality (default: true)
|
||||
pub show_upload: Option<bool>,
|
||||
/// Whether to show download functionality (default: true)
|
||||
pub show_download: Option<bool>,
|
||||
/// Whether to show delete functionality (default: true)
|
||||
pub show_delete: Option<bool>,
|
||||
/// Theme: "light" or "dark" (default: "light")
|
||||
pub theme: Option<String>,
|
||||
/// Custom CSS classes to apply to the root element
|
||||
pub css_classes: Option<String>,
|
||||
/// Initial path to display (default: "")
|
||||
pub initial_path: Option<String>,
|
||||
}
|
||||
|
||||
/// JavaScript-compatible configuration wrapper
|
||||
#[wasm_bindgen]
|
||||
pub struct JSWidgetConfig {
|
||||
inner: WidgetConfig,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl JSWidgetConfig {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(base_endpoint: String) -> JSWidgetConfig {
|
||||
JSWidgetConfig {
|
||||
inner: WidgetConfig {
|
||||
base_endpoint,
|
||||
max_file_size: None,
|
||||
show_upload: None,
|
||||
show_download: None,
|
||||
show_delete: None,
|
||||
theme: None,
|
||||
css_classes: None,
|
||||
initial_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setMaxFileSize)]
|
||||
pub fn set_max_file_size(&mut self, size: f64) {
|
||||
self.inner.max_file_size = Some(size as u64);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setShowUpload)]
|
||||
pub fn set_show_upload(&mut self, show: bool) {
|
||||
self.inner.show_upload = Some(show);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setShowDownload)]
|
||||
pub fn set_show_download(&mut self, show: bool) {
|
||||
self.inner.show_download = Some(show);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setShowDelete)]
|
||||
pub fn set_show_delete(&mut self, show: bool) {
|
||||
self.inner.show_delete = Some(show);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setTheme)]
|
||||
pub fn set_theme(&mut self, theme: String) {
|
||||
self.inner.theme = Some(theme);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setCssClasses)]
|
||||
pub fn set_css_classes(&mut self, classes: String) {
|
||||
self.inner.css_classes = Some(classes);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setInitialPath)]
|
||||
pub fn set_initial_path(&mut self, path: String) {
|
||||
self.inner.initial_path = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WidgetConfig> for FileBrowserConfig {
|
||||
fn from(widget_config: WidgetConfig) -> Self {
|
||||
FileBrowserConfig {
|
||||
base_endpoint: widget_config.base_endpoint,
|
||||
max_file_size: widget_config.max_file_size.unwrap_or(100 * 1024 * 1024), // 100MB default
|
||||
show_upload: widget_config.show_upload.unwrap_or(true),
|
||||
show_download: widget_config.show_download.unwrap_or(true),
|
||||
show_delete: widget_config.show_delete.unwrap_or(true),
|
||||
show_create_dir: true, // Always enable directory creation
|
||||
theme: widget_config.theme.unwrap_or_else(|| "light".to_string()),
|
||||
css_classes: widget_config.css_classes.unwrap_or_default(),
|
||||
initial_path: widget_config.initial_path.unwrap_or_default(),
|
||||
chunk_size: 1024 * 1024, // 1MB chunks for uploads
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget wrapper component
|
||||
#[function_component(FileBrowserWidget)]
|
||||
fn file_browser_widget(props: &WidgetConfig) -> Html {
|
||||
let config: FileBrowserConfig = props.clone().into();
|
||||
|
||||
html! {
|
||||
<div class="file-browser-widget">
|
||||
<FileBrowser ..config />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle for managing the widget instance
|
||||
#[wasm_bindgen]
|
||||
pub struct FileBrowserWidgetHandle {
|
||||
app_handle: yew::AppHandle<FileBrowserWidget>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl FileBrowserWidgetHandle {
|
||||
/// Destroy the widget instance
|
||||
pub fn destroy(self) {
|
||||
// The app handle will be dropped, cleaning up the widget
|
||||
}
|
||||
|
||||
/// Update the widget configuration
|
||||
pub fn update_config(&self, _config: JSWidgetConfig) {
|
||||
// For now, we'll need to recreate the widget to update config
|
||||
// In a more advanced implementation, we could send messages to update config
|
||||
web_sys::console::warn_1(&"Config updates require recreating the widget instance".into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Create and mount a FileBrowser widget to the specified DOM element
|
||||
#[wasm_bindgen]
|
||||
pub fn create_file_browser_widget(
|
||||
container_id: &str,
|
||||
config: JSWidgetConfig,
|
||||
) -> Result<FileBrowserWidgetHandle, JsValue> {
|
||||
let document = web_sys::window()
|
||||
.ok_or("No global window object")?
|
||||
.document()
|
||||
.ok_or("No document object")?;
|
||||
|
||||
let container = document
|
||||
.get_element_by_id(container_id)
|
||||
.ok_or_else(|| format!("Element with id '{}' not found", container_id))?;
|
||||
|
||||
let app_handle = yew::Renderer::<FileBrowserWidget>::with_root_and_props(
|
||||
container,
|
||||
config.inner,
|
||||
).render();
|
||||
|
||||
Ok(FileBrowserWidgetHandle { app_handle })
|
||||
}
|
||||
|
||||
/// Create and mount a FileBrowser widget to a specific DOM element
|
||||
#[wasm_bindgen]
|
||||
pub fn create_file_browser_widget_on_element(
|
||||
element: &Element,
|
||||
config: JSWidgetConfig,
|
||||
) -> Result<FileBrowserWidgetHandle, JsValue> {
|
||||
let app_handle = yew::Renderer::<FileBrowserWidget>::with_root_and_props(
|
||||
element.clone(),
|
||||
config.inner,
|
||||
).render();
|
||||
|
||||
Ok(FileBrowserWidgetHandle { app_handle })
|
||||
}
|
||||
|
||||
/// Utility function to create a default configuration
|
||||
#[wasm_bindgen]
|
||||
pub fn create_default_config(base_endpoint: &str) -> JSWidgetConfig {
|
||||
JSWidgetConfig::new(base_endpoint.to_string())
|
||||
}
|
||||
|
||||
/// Get version information
|
||||
#[wasm_bindgen]
|
||||
pub fn get_version() -> String {
|
||||
env!("CARGO_PKG_VERSION").to_string()
|
||||
}
|
||||
|
||||
/// Check if the widget is compatible with the current browser
|
||||
#[wasm_bindgen]
|
||||
pub fn check_browser_compatibility() -> bool {
|
||||
// Basic compatibility checks
|
||||
let window = web_sys::window();
|
||||
if window.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let window = window.unwrap();
|
||||
|
||||
// Check for required APIs
|
||||
let has_fetch = js_sys::Reflect::has(&window, &"fetch".into()).unwrap_or(false);
|
||||
let has_blob = js_sys::Reflect::has(&window, &"Blob".into()).unwrap_or(false);
|
||||
let has_form_data = js_sys::Reflect::has(&window, &"FormData".into()).unwrap_or(false);
|
||||
|
||||
has_fetch && has_blob && has_form_data
|
||||
}
|
Loading…
Reference in New Issue
Block a user