Widget System
Nagelfluh's frontend uses a flexible widget system where each pane in the layout can display different types of content. Widgets are React components that can be dragged, dropped, and rearranged within the Flexout layout system.
Widget Basics
What is a Widget?
A widget is a React component that:
- Renders content in a layout pane
- Has a static title property for display in menus
- Can access global state via React Context
- Can be instantiated multiple times in different panes
Widget Registration
Widgets are registered in the widgets object in frontend/src/App.js.
See: frontend/src/App.js - look for the widgets constant where all built-in widgets are imported and registered.
Creating a New Widget
Basic Widget Template
// MyWidget.js
import React from 'react';
import { useProcessContext } from './ProcessContext';
function MyWidget() {
const { processes, activeProcess } = useProcessContext();
return (
<div style={{ padding: '10px' }}>
<h3>My Custom Widget</h3>
<p>Active process: {activeProcess?.processId || 'None'}</p>
{/* Your widget content here */}
</div>
);
}
// IMPORTANT: Set the static title property
MyWidget.title = "My Widget";
export default MyWidget;
Register the Widget
Add to App.js:
import MyWidget from './MyWidget';
const widgets = {
// ... existing widgets
MyWidget,
};
The widget will now appear in the dropdown menu of every pane.
Built-in Widgets
FlowView
Visual graph of processes and their dependencies.
Features: - ReactFlow-based node graph - Shows process connections (input → process → output) - Click node to set as active process - Drag nodes to rearrange layout - Auto-layout on process changes
Usage:
const { processes, activeProcess, setActiveProcess } = useProcessContext();
Key interactions: - Click process node → sets active process - Drag nodes → repositions in graph - Zoom/pan → navigate large graphs
ProcessEditor
Dual-mode editor for creating and editing processes.
Mode 1: Create Mode (no active process) - Select process type from dropdown - Configure resources (CPU, memory, deadline) - See estimated max cost - Fill parameter form (JSON Schema-based) - Submit to create process
Mode 2: Edit Mode (active process selected) - View current process parameters - Edit parameters - Create new version with changes - View output datasets
Features: - JSON Schema form rendering via @rjsf/core - Custom fields for dataset selection - Resource validation - Cost calculation - Version history
Data Access Pattern:
const { processes, activeProcess } = useProcessContext();
// Find the full process object
const process = processes.find(p => p.id === activeProcess?.processId);
// Access process data directly
const version = process?.versions[activeProcess?.version];
const outputs = version?.outputs; // { "output_name": "url" }
const parameters = version?.parameters;
const state = version?.state; // "pending", "running", "completed", "failed"
ProcessLog
Real-time log viewer with WebSocket streaming.
Features: - Live log updates via WebSocket - Filter by process - Status badges (Running, Completed, Failed) - Auto-scroll to latest - Persistent log history
Implementation: See frontend/src/widgets/ProcessLog.js - uses WebSocket connection to backend for real-time log streaming.
PlotView
Plotly-based scientific plotting with extensible element system.
Architecture: - Plot Elements Registry: Pluggable element types - Unit Matching: Automatic axis assignment - Dataset Integration: Direct dataset loading - Dynamic Traces: Builds Plotly traces from data
Plot Element Structure:
Plot elements are defined in frontend/src/widgets/PlotView/elements/ directory. Each element exports:
- x_unit and y_unit - For axis matching
- parameters - JSON Schema for configuration
- render() - Function that returns Plotly trace object
See: frontend/src/widgets/PlotView/elements/index.js for the registry of all plot elements.
Plot Element data_context:
IMPORTANT: When a plot element's get_schema() method is called, the data_context parameter contains all entries from ProcessContext. This includes:
{
processes, // Array of all process objects
activeProcess, // { processId, version } or null
datasets, // Array of dataset objects (from useProcessOutputDatasets)
datasetObjects, // Loaded dataset objects
fetchedData, // Fetched dataset data
currentProject, // Current project ID
// ... and all other ProcessContext values
}
To get dataset names in get_schema(), extract them from process outputs:
get_schema: (data_context = {}) => {
const processes = data_context.processes || [];
// Extract all output dataset names from all processes
const datasetNames = [];
processes.forEach(proc => {
proc.versions?.forEach(ver => {
if (ver.outputs) {
datasetNames.push(...Object.keys(ver.outputs));
}
});
});
return {
// ... schema with datasetNames in enum
};
}
❌ DON'T use data_context.datasets array and map d.dataset_name - this is the old pattern.
✅ DO use data_context.processes and extract output names from process.versions[x].outputs keys.
Adding a Plot Element:
- Create new file in
frontend/src/widgets/PlotView/elements/ - Export an object with
xaxis,yaxis,get_schema(), andrender()function - Register in
frontend/src/widgets/PlotView/elements/index.js
See existing examples:
- frontend/src/widgets/PlotView/elements/ChannelPlot.js
- frontend/src/widgets/PlotView/elements/FlightlinePlot.js
- frontend/src/widgets/PlotView/elements/ResistivityCurtain.js
MapView
Geographic visualization of survey data.
Features: - Interactive map with layers - Display flight lines - Show data coverage - Geographic coordinate handling
Widget State Management
Using ProcessContext
Most widgets need access to process data:
import { useProcessContext } from './ProcessContext';
function MyWidget() {
const {
processes, // Array of all processes
activeProcess, // { processId, version } or null
setActiveProcess, // Function to set active process
createProcess, // Function to create new process
} = useProcessContext();
// Access full process data
const process = processes.find(p => p.id === activeProcess?.processId);
const version = process?.versions[activeProcess?.version];
return (
<div>
{version && (
<>
<h4>{process.name}</h4>
<p>State: {version.state}</p>
<p>Outputs: {JSON.stringify(version.outputs)}</p>
</>
)}
</div>
);
}
Process Object Structure
{
id: "process-abc-123",
name: "FFT Analysis",
type: "fft",
versions: [
{
version: 0,
parameters: { /* JSON Schema params */ },
outputs: {
"spectrum": "http://localhost:8000/dataset/xyz-789"
},
state: "completed", // "pending" | "running" | "completed" | "failed"
logs: [
{ timestamp: "2024-01-01T12:00:00Z", message: "Starting..." },
// ...
]
}
]
}
Local Widget State
Widgets can maintain their own local state:
function MyWidget() {
const [selectedItem, setSelectedItem] = useState(null);
const [filters, setFilters] = useState({ showAll: true });
// Widget state persists while widget is mounted
// State is lost when widget is removed from layout
}
Persistent Widget Configuration
For configuration that should persist across sessions, use the layout node's data:
// In LayoutContext, each node can have custom data
{
id: "pane-123",
widget: "PlotView",
data: {
plotElements: [
{ type: "Line", params: { dataset: "..." } }
]
}
}
// Access in widget:
function PlotView({ nodeId }) {
const { getNode, updateNode } = useLayoutContext();
const node = getNode(nodeId);
const plotElements = node.data?.plotElements || [];
const addElement = (element) => {
updateNode(nodeId, {
data: {
...node.data,
plotElements: [...plotElements, element]
}
});
};
}
Widget Communication
Via Active Process
The primary communication mechanism is through the active process:
// ProcessEditor: User creates/edits process
const { setActiveProcess, createProcess } = useProcessContext();
await createProcess(type, params);
setActiveProcess({ processId: newId, version: 0 });
// FlowView: Shows visual feedback
const { activeProcess } = useProcessContext();
// Highlights active process node
// PlotView: Displays active process outputs
const { processes, activeProcess } = useProcessContext();
const outputs = processes.find(p => p.id === activeProcess.processId)
?.versions[activeProcess.version]?.outputs;
Via Custom Context
For widget-specific communication, create custom contexts:
// MapContext.js
const MapContext = createContext();
export function MapProvider({ children }) {
const [selectedLocation, setSelectedLocation] = useState(null);
return (
<MapContext.Provider value={{ selectedLocation, setSelectedLocation }}>
{children}
</MapContext.Provider>
);
}
// Use in multiple widgets
function MapView() {
const { setSelectedLocation } = useContext(MapContext);
// ...
}
function LocationInfo() {
const { selectedLocation } = useContext(MapContext);
// ...
}
Best Practices
Data Access
✅ DO: Access process data directly from the processes array
const process = processes.find(p => p.id === activeProcess.processId);
const outputs = process?.versions[activeProcess.version]?.outputs;
❌ DON'T: Assume data exists in complex abstractions
// Don't create unnecessary intermediate state
const [currentOutputs, setCurrentOutputs] = useState({});
Performance
✅ DO: Memoize expensive computations
const processedData = useMemo(() => {
return heavyComputation(rawData);
}, [rawData]);
❌ DON'T: Fetch data in render
// Don't do this - causes infinite re-renders
const MyWidget = () => {
const data = fetch(url).then(r => r.json()); // ❌ Wrong!
return <div>{data}</div>;
};
Error Handling
✅ DO: Handle loading and error states
function MyWidget() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{/* render data */}</div>;
}
Styling
✅ DO: Use inline styles or CSS modules for widget-specific styling
// Inline styles for simple cases
<div style={{ padding: '10px', backgroundColor: '#f0f0f0' }}>
// CSS modules for complex styling
import styles from './MyWidget.module.css';
<div className={styles.container}>
❌ DON'T: Use global CSS that might conflict
/* ❌ Too generic, might conflict */
.container { padding: 10px; }
/* ✅ Widget-specific */
.myWidget-container { padding: 10px; }
Advanced Topics
Widget Props
Widgets receive props from the layout system:
function MyWidget({ nodeId, onClose, onPopout }) {
// nodeId: Unique identifier for this pane
// onClose: Function to close this pane
// onPopout: Function to popout this pane to new window
return (
<div>
<button onClick={onClose}>Close Me</button>
<button onClick={onPopout}>Popout</button>
</div>
);
}
Widget Lifecycle
function MyWidget() {
// Runs on mount
useEffect(() => {
console.log('Widget mounted');
// Cleanup on unmount
return () => {
console.log('Widget unmounted');
};
}, []);
// Runs when dependencies change
useEffect(() => {
console.log('Active process changed');
}, [activeProcess]);
}
Multiple Instances
Widgets can be instantiated multiple times:
// Two PlotView widgets can show different plots
// Each maintains its own state and configuration
<Layout>
<Pane widget="PlotView" nodeId="plot-1" />
<Pane widget="PlotView" nodeId="plot-2" />
</Layout>
Use nodeId to distinguish between instances:
function PlotView({ nodeId }) {
const config = loadConfig(nodeId); // Load instance-specific config
// ...
}