Save User Filter Configurations with Omni Events messaging

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:filters events 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 saveToStorage with 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 asJsonUrlSearchParam from each filter in the saved configuration
  • Joins filter parameters with & to create a URL parameter string
  • Uses the dashboard:filter-change-by-url-parameter event
  • 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

  1. Origin Validation: Always validate that messages come from Omni domains
  2. Source Validation: Only process messages with source: 'omni'
  3. User Isolation: If using a database, ensure configurations are tied to specific users
  4. 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:filters event name is correct

Load Configuration Not Working

  • Verify asJsonUrlSearchParam exists in saved filters
  • Check console for postMessage being sent
  • Ensure iframe is fully loaded before attempting to load config
  • Verify filterUrlParameter is 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+

Additional Resources

1 Like