# Save User Filter Configurations with Omni Events messaging

**URL:** https://community.omni.co/t/save-user-filter-configurations-with-omni-events-messaging/351
**Category:** Embedded Analytics
**Tags:** dashboards, embed
**Created:** [July 3, 2025, 10:24pm UTC](https://community.omni.co/t/save-user-filter-configurations-with-omni-events-messaging/351 "2025-07-03T22:24:44Z")
**Posts on this page:** 2
**Page:** 1

<div class="post-metadata">

### Author: ![aklance](https://yyz2.discourse-cdn.com/flex004/user_avatar/community.omni.co/aklance/32/132_2.png) [@aklance](https://community.omni.co/u/aklance)
#### Post date: [July 3, 2025, 10:24pm UTC](https://community.omni.co/t/save-user-filter-configurations-with-omni-events-messaging/351/1 "2025-07-03T22:24:44Z")

</div>

# 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
https://www.loom.com/embed/b64e9cfcca8f437187440b381e4b9915
## 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:**

```typescript
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:**

```javascript
{
  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:**

```typescript
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:**

```typescript
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:**

```javascript
{
  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:

```typescript
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

```typescript
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

```typescript
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

```typescript
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:

```tsx
<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

- [Omni Embed Documentation](https://docs.omni.co/docs/embed/external-embedding/customization-and-interactivity)
- [MDN postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
- [MDN localStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)

---

<div class="post-metadata">

### Author: ![ShivaPrasad](https://yyz2.discourse-cdn.com/flex004/user_avatar/community.omni.co/shivaprasad/32/651_2.png) [@ShivaPrasad](https://community.omni.co/u/ShivaPrasad)
#### Post date: [May 2, 2026, 2:25am UTC](https://community.omni.co/t/save-user-filter-configurations-with-omni-events-messaging/351/2 "2026-05-02T02:25:15Z")

</div>

# Follow-up: Gotchas We Found When Implementing Filter Persistence

Great guide! For anyone implementing this, here are some non-obvious issues we ran into:

### 1. **Filter Normalization & Display-Only Fields**

Omni re-emits `dashboard:filters` events with normalized JSON and sometimes includes display-only fields like `appliedLabels`. This means byte-equal comparison of filter strings is unreliable.

**Fix:** Parse and canonicalize both the saved and current filter params before comparing. Sort keys alphabetically, recursively re-serialize JSON, and filter out display-only keys:

```typescript
const DISPLAY_ONLY_KEYS = new Set(['appliedLabels'])

function canonicalJson(value: unknown): string {
  if (value && typeof value === 'object') {
    const keys = Object.keys(value)
      .filter((key) => !DISPLAY_ONLY_KEYS.has(key))
      .sort()
    return `{${keys.map(k => `${JSON.stringify(k)}:${canonicalJson((value as any)[k])}`).join(',')}}`
  }
  return JSON.stringify(value)
}

```

This lets you reliably detect whether the user’s current filters match a saved configuration.

### 2. **Omni Emits All Filters, Even Inactive Ones**

The `dashboard:filters` event includes every filter on the dashboard, not just the ones the user has actively set. You can’t treat presence in the payload as “active.”

**Fix:** Inspect the filter values themselves:

- For list filters: check if `values` array has length \> 0
- For date ranges: check if `left_side` or `right_side` are non-null
- Only treat filters as “active” if they contain actual selections

### 3. **Race Condition on Initial Load**

If you try to apply a saved filter immediately when the dashboard loads, you might race Omni’s initialization. The dashboard can emit `dashboard:filters` events during boot as it’s settling into its initial state.

**Fix:** Defer applying saved filters until _after_ the first `dashboard:filters` event. This way, Omni has already emitted its baseline state before you overlay your saved configuration. Use a flag like `omniReady` that’s set when you receive the first `size` postMessage (indicating Omni JS is warm and ready for commands).

### 4. **Iframe Readiness vs Component Readiness**

Don’t assume `iframeRef.current?.contentWindow` exists just because your component mounted. The iframe needs to finish loading and Omni’s JS needs to initialize before it can receive postMessage commands.

**Fix:** Wait for an initial `size` message (or equivalent lifecycle event) from Omni before sending filter-change commands. Queue pending commands and only flush them once the iframe is ready.

### 5. **Clearing Filters Isn’t Supported**

As mentioned in the guide, Omni doesn’t currently support programmatically clearing all filters via a postMessage event. Empty `filterUrlParameter` won’t reset the dashboard.

**Fix:** When the user wants to clear filters, force a fresh iframe mount. Trigger a key change or URL re-sign so the dashboard reloads in its default state. You lose your scroll position, but it’s the most reliable way to get a clean slate.

### 6. **Cache Invalidation on Dashboard Switch**

If your app caches filter configs in localStorage, make sure to clear the cache when switching dashboards. Filter params are dashboard-specific and won’t work across different dashboards.

### 7. **User/Organization Isolation**

Store saved filter configs on the backend tied to user ID, organization, and potentially region/platform. Don’t rely on client-side-only storage if you want configs to persist across devices or be managed per user.

Hope this helps anyone building on top of this pattern!
