add file browser component and widget

This commit is contained in:
Timur Gordon
2025-08-05 15:02:23 +02:00
parent 4e43c21b72
commit ba43a82db0
95 changed files with 17840 additions and 423 deletions

View 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.

View 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>

View 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>;

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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

File diff suppressed because one or more lines are too long

69
examples/widget_example/uppy.min.js vendored Normal file

File diff suppressed because one or more lines are too long