Saving and Restoring Dashboard Filter Configurations with Omni Events Messaging for Embedded Dashboards
This guide explains how to save and restore user filter configurations for embedded dashboards, allowing your users to return to their preferred dashboard views within your embedded application without needing to be restricted_queriers.
Overview
When users interact with embedded dashboards, they often apply filters to customize their view. By capturing these filter changes, you can save their preferences and restore them in future sessions, creating a personalized experience. This implementation uses Omni’s event messaging system to capture filter states and apply them programmatically.
Prerequisites
- Be an admin user in Omni
- Have the ability to embed Omni dashboards
- Enable vanity domains - This eliminates cross-origin restrictions that would block communication between your application and the embedded dashboard. Both your application and the dashboard need proper DNS configuration.
Benefits of This Approach
This event-based solution offers several advantages:
- Flexibility - Customize the save/restore experience to match your application’s design
- Integration - The feature lives within your application’s existing UI patterns
- User Experience - Provides a seamless, cohesive experience for users working with embedded dashboards
- Persistence - Filter configurations survive across browser sessions
- Management - Users can create, update, rename, duplicate, and delete saved configurations
Video Demo
Implementation Steps
Step 1: Set Up Communication
Implement the postMessage protocol in your application to enable two-way communication with the embedded dashboard. This allows your application to:
- Listen for events from the dashboard
- Send stored events back to the dashboard
Step 2: Listen to Dashboard Filter Events
Configure your application to listen for filter change events by setting up an event listener for the dashboard:filters event.
Key Code Implementation:
useEffect(() => {
const listener = (event: MessageEvent) => {
// Validate message origin to ensure it's from Omni
if (!event.origin.includes('omniapp.co') && !event.origin.includes('omniapp.com')) {
console.log("Ignoring message from non-Omni origin");
return;
}
// Only process messages with the 'omni' source
if (event.data && event.data.source !== 'omni') {
return;
}
// Handle filter events
if (event.data.name === "dashboard:filters") {
const filters = event.data.payload;
setCurrentFilters(filters);
console.log("âś… Filter state captured:", filters);
}
};
window.addEventListener("message", listener);
return () => window.removeEventListener("message", listener);
}, []);
What This Does:
- Validates that messages come from Omni domains (security best practice)
- Checks for
source: 'omni'in the message data - Captures the complete filter payload when
dashboard:filtersevents occur - Stores filter state for later saving
Event Payload Structure:
{
source: "omni",
name: "dashboard:filters",
payload: {
"filter.field.name": {
filter: { /* filter configuration */ },
asJsonUrlSearchParam: "f--filter.field.name=%7B..."
}
}
}
Step 3: Save Filter Configurations
When a filter change event occurs, allow users to save the configuration with a custom name.
Implementation:
interface FilterConfig {
id: string;
name: string;
filters: any;
createdAt: string;
updatedAt: string;
}
const STORAGE_KEY = "omni-filter-configs";
const confirmSaveConfig = () => {
if (!configName.trim()) {
alert("Please enter a name for this configuration");
return;
}
const newConfig: FilterConfig = {
id: Date.now().toString(),
name: configName.trim(),
filters: currentFilters,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const updatedConfigs = [...savedConfigs, newConfig];
saveToStorage(updatedConfigs);
setConfigName("");
setShowSaveDialog(false);
setSelectedConfigId(newConfig.id);
};
const saveToStorage = (configs: FilterConfig[]) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(configs));
setSavedConfigs(configs);
};
What This Does:
- Creates a unique ID using timestamp
- Stores the complete filter object from Omni
- Persists to localStorage (can be adapted to use a database)
- Automatically selects the newly saved configuration
Storage Options:
- LocalStorage (shown above) - Simple, client-side only, suitable for single-user scenarios
- Database - For multi-user applications, replace
saveToStoragewith an API call to your backend
Step 4: Restore Saved Configurations
To restore a user’s saved filter preferences, send the stored configuration back to the dashboard.
Implementation:
const handleLoadConfig = (configId: string) => {
const config = savedConfigs.find((c) => c.id === configId);
if (!config) {
console.error("Config not found:", configId);
return;
}
if (!iframeRef.current?.contentWindow) {
console.error("Iframe not available");
alert("Dashboard iframe not ready. Please wait and try again.");
return;
}
setSelectedConfigId(configId);
// Convert the filters object to filterUrlParameter string
// Each filter has an asJsonUrlSearchParam that needs to be concatenated
const filterParams = Object.values(config.filters)
.map((filter: any) => filter.asJsonUrlSearchParam)
.join("&");
// Send the filter configuration to the dashboard
iframeRef.current.contentWindow.postMessage(
{
name: "dashboard:filter-change-by-url-parameter",
payload: {
filterUrlParameter: filterParams
},
},
"*" // Use wildcard since iframe is on embed-omniapp.co
);
console.log("âś… Loaded filter configuration:", config.name);
};
What This Does:
- Extracts
asJsonUrlSearchParamfrom each filter in the saved configuration - Joins filter parameters with
&to create a URL parameter string - Uses the
dashboard:filter-change-by-url-parameterevent - Sends the configuration to the iframe via postMessage
- The dashboard automatically updates to match the saved configuration
Event Sent to Dashboard:
{
name: "dashboard:filter-change-by-url-parameter",
payload: {
filterUrlParameter: "f--field1=...&f--field2=..."
}
}
Complete Feature Set
A full implementation can include these management features:
1. Update Configuration
Update an existing configuration with current filter values:
const handleUpdateConfig = () => {
if (!selectedConfigId) return;
const configIndex = savedConfigs.findIndex(c => c.id === selectedConfigId);
if (configIndex === -1) return;
const updatedConfigs = [...savedConfigs];
updatedConfigs[configIndex] = {
...updatedConfigs[configIndex],
filters: currentFilters,
updatedAt: new Date().toISOString(),
};
saveToStorage(updatedConfigs);
};
2. Rename Configuration
const handleRenameConfig = (configId: string, newName: string) => {
const updatedConfigs = savedConfigs.map(config =>
config.id === configId
? { ...config, name: newName, updatedAt: new Date().toISOString() }
: config
);
saveToStorage(updatedConfigs);
};
3. Duplicate Configuration
const handleDuplicateConfig = (configId: string) => {
const config = savedConfigs.find(c => c.id === configId);
if (!config) return;
const newConfig: FilterConfig = {
id: Date.now().toString(),
name: `${config.name} (Copy)`,
filters: JSON.parse(JSON.stringify(config.filters)), // Deep clone
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
saveToStorage([...savedConfigs, newConfig]);
};
4. Delete Configuration
const handleDeleteConfig = (configId: string) => {
const updatedConfigs = savedConfigs.filter(c => c.id !== configId);
saveToStorage(updatedConfigs);
// Clear selection if deleting the currently selected config
if (selectedConfigId === configId) {
setSelectedConfigId(null);
}
};
User Interface Example
Here’s a basic UI structure for managing filter configurations:
<div className="filter-config-container">
{/* Toolbar */}
<div className="filter-config-toolbar">
<div className="status-indicator">
{currentFilters ? "✓ Filters captured" : "⏳ Waiting for filters..."}
</div>
<button onClick={() => setShowSaveDialog(true)} disabled={!currentFilters}>
đź’ľ Save Current Filters
</button>
<button onClick={handleUpdateConfig} disabled={!selectedConfigId}>
🔄 Update Selected
</button>
</div>
{/* Configuration Cards */}
<div className="config-grid">
{savedConfigs.map((config) => (
<div
key={config.id}
className={`config-card ${selectedConfigId === config.id ? 'selected' : ''}`}
>
<h3>{config.name}</h3>
<div className="config-metadata">
<small>Created: {new Date(config.createdAt).toLocaleString()}</small>
<small>Updated: {new Date(config.updatedAt).toLocaleString()}</small>
</div>
<div className="config-actions">
<button onClick={() => handleLoadConfig(config.id)}>▶️ Load</button>
<button onClick={() => handleRenameConfig(config.id)}>✏️ Rename</button>
<button onClick={() => handleDuplicateConfig(config.id)}>đź“‹ Duplicate</button>
<button onClick={() => handleDeleteConfig(config.id)}>🗑️ Delete</button>
</div>
</div>
))}
</div>
{/* Embedded Dashboard */}
<iframe
ref={iframeRef}
src={dashboardUrl}
style={{ width: '100%', height: '800px', border: 'none' }}
/>
</div>
Security Considerations
- Origin Validation: Always validate that messages come from Omni domains
- Source Validation: Only process messages with
source: 'omni' - User Isolation: If using a database, ensure configurations are tied to specific users
- No Sensitive Data: Filter configurations contain filter values but no authentication tokens
Known Limitations
- Clear All Filters: Omni’s embed API does not currently support programmatically clearing all filters via postMessage events. Users must manually clear filters in the dashboard. Or create a base state saved filter configuration.
Troubleshooting
Filters Not Being Captured
- Check browser console for postMessage events
- Verify iframe origin matches validation logic
- Ensure
source: 'omni'is present in message data - Confirm
dashboard:filtersevent name is correct
Load Configuration Not Working
- Verify
asJsonUrlSearchParamexists in saved filters - Check console for postMessage being sent
- Ensure iframe is fully loaded before attempting to load config
- Verify
filterUrlParameteris correctly formatted
LocalStorage Issues
- Check browser localStorage quota
- Verify no browser extensions blocking localStorage
- Test in incognito mode to rule out extensions
Technical Requirements
- Modern browsers with postMessage API support
- localStorage support (or backend database)
- React 18.2.0+ (or equivalent framework)
- TypeScript 5.1.6+ (optional but recommended)
- Omni Embed SDK 0.3.18+