Skip to content

[Discover Sessions as Code] Discover search embeddable: API schema, transforms, and feature flag#255213

Merged
lukasolson merged 59 commits into
elastic:mainfrom
lukasolson:discover_sessions_as_code/embeddable_transforms
Apr 2, 2026
Merged

[Discover Sessions as Code] Discover search embeddable: API schema, transforms, and feature flag#255213
lukasolson merged 59 commits into
elastic:mainfrom
lukasolson:discover_sessions_as_code/embeddable_transforms

Conversation

@lukasolson
Copy link
Copy Markdown
Contributor

@lukasolson lukasolson commented Feb 27, 2026

Summary

Resolves #240164.
Resolves #248926.
Resolves #248927.

Implements Discover Session search embeddable support for the Dashboards / embeddables “as code” direction: bidirectional conversion between persisted Saved Search panel state (legacy savedObjectId / attributes.tabs shape) and a simplified API schema (discover_session_id, selected_tab_id, tabs, overrides, snake_case panel fields, as-code filters and data sources). Registers a config-schema for that API shape (when the feature flag is on) so server-side validation and OpenAPI-style docs can describe by-value vs by-reference panels clearly.


Feature flag

  • discover.embeddableTransforms (EMBEDDABLE_TRANSFORMS_FEATURE_FLAG_KEY in discover/common/constants.ts)
  • When off (default): behavior matches pre-PR paths: transformOut uses the legacy panel shape; transformIn only runs the legacy Saved Search pipeline; no embeddable schema is registered from Discover server; client serialization keeps the legacy shape.
  • When on: transformOut emits the simplified Discover Session embeddable state; transformIn accepts either legacy panel state or API state and normalizes to stored shape; Discover server supplies getDiscoverSessionEmbeddableSchema; client serializeState / deserializeState read and write the API types end-to-end for the running embeddable.

Transforms (common)

Central logic lives in discover/common/embeddable/:

Direction Role
Stored → API savedSearchToDiscoverSessionEmbeddableState (+ byValue* / byReference* helpers) used from getTransformOut when the flag is enabled.
API → stored discoverSessionToSavedSearchEmbeddableState (+ helpers) used from getTransformIn when input is already API-shaped.
Legacy path legacyTransformIn / legacyTransformOut preserve previous behavior for BWC; SearchEmbeddablePanelApiState is a union of legacy and API state (types.ts).

Type guards (type_guards.ts) distinguish legacy vs API panel state so drilldown transforms and the rest of the pipeline stay correct.

Integration with as-code packages:

  • Filters: fromStoredFilters / toStoredFilters (@kbn/as-code-filters-transforms).
  • Data views / data source: fromStoredDataView / toStoredDataView (@kbn/as-code-data-views-transforms) with a shared discriminated data source schema (reference vs inline spec, optional runtime fields via schema_runtime_field).

By-value vs by-reference semantics

  • By-value API: single-tab model exposed as tabs (length 1 today). Panel-level table options merge with that tab on stored → API; API → stored and client deserialize rebuild Saved Search attributes from tabs[0] plus overrides.
  • By-reference API: discover_session_id, optional selected_tab_id, and overrides for dashboard-level edits (sort, columns, grid, row heights, sample size, etc.) instead of duplicating those at the top level of the API object.
  • Client runtime: deserializeState maps API fields (discover_session_idsavedObjectId, etc.), applies overrides into the runtime Saved Search shape, and keeps title / time range / drilldowns behavior aligned with the publishing layer.

Server: schema registration

  • discover/server/embeddable/schema.ts defines classic vs ES|QL tab schemas (query + filters + data_source + view_mode vs aggregateQuerySchema), panel overrides, drilldown intersection with serializedTitlesSchema, serializedTimeRangeSchema, and drilldown schema from the embeddable host.
  • By-value / by-reference branches use BY_VALUE_SCHEMA_META and BY_REF_SCHEMA_META from @kbn/presentation-publishing-schemas so oneOf / “Any of” documentation shows human-readable option labels.
  • DiscoverServerPlugin registers getSchema only when the feature flag is enabled (avoids registering API schema when the feature is off).

Exported TypeOf types from that schema are the source of truth for DiscoverSessionEmbeddable* types consumed by transforms and the public embeddable factory typing.


Client embeddable

Previously, by-value Discover search panels could fail to save dashboard-level changes reliably: what we wrote back into the panel JSON didn’t line up with how the embeddable deserialized and compared state, so edits didn’t round-trip cleanly.

With discover.embeddableTransforms enabled, serialize / deserialize use the same Discover Session API shape as transformIn / transformOut: by-value state is anchored on tabs[0] (with panel fields merged consistently), and serialization emits that shape instead of the older Saved Search–only panel blob. That alignment fixes save-and-return and normal dashboard save for by-value panels so changes actually stick.


Example API calls

---------------------------------------------------------------------------
By reference — minimal (first tab, no overrides)
---------------------------------------------------------------------------
POST kbn:/api/dashboards?apiVersion=1
{
  "title": "Minimal by-ref Discover panel",
  "panels": [
    {
      "grid": { "x": 0, "y": 0, "w": 48, "h": 15 },
      "type": "discover_session",
      "config": {
        "title": "Saved session",
        "discover_session_id": "REPLACE_WITH_SEARCH_SAVED_OBJECT_ID"
      }
    }
  ]
}

---------------------------------------------------------------------------
By reference — overrides only in API shape (sort, columns, pagination, auto heights)
---------------------------------------------------------------------------
# `overrides` can include: column_order, column_settings, sort, density, header_row_height,
# row_height (numbers or "auto"), rows_per_page (10|25|50|100|250|500), sample_size (10–10000).

POST kbn:/api/dashboards?apiVersion=1
{
  "title": "By-ref with rich overrides",
  "panels": [
    {
      "grid": { "x": 0, "y": 0, "w": 48, "h": 18 },
      "type": "discover_session",
      "config": {
        "title": "Session + dashboard-specific table",
        "discover_session_id": "REPLACE_WITH_SEARCH_SAVED_OBJECT_ID",
        "selected_tab_id": "REPLACE_TAB_ID_OPTIONAL",
        "overrides": {
          "sort": [{ "name": "@timestamp", "direction": "desc" }],
          "column_order": ["message", "@timestamp", "host.name"],
          "column_settings": { "message": { "width": 420 } },
          "density": "compact",
          "header_row_height": "auto",
          "row_height": "auto",
          "rows_per_page": 50,
          "sample_size": 500
        }
      }
    }
  ]
}

---------------------------------------------------------------------------
By value — classic tab with data view reference + view modes (documents / patterns / aggregated)
---------------------------------------------------------------------------
# view_mode: "documents" | "patterns" | "aggregated"

POST kbn:/api/dashboards?apiVersion=1
{
  "title": "Inline Discover — patterns mode",
  "panels": [
    {
      "grid": { "x": 0, "y": 0, "w": 48, "h": 15 },
      "type": "discover_session",
      "config": {
        "title": "Patterns (by value)",
        "tabs": [
          {
            "sort": [],
            "column_order": [],
            "density": "normal",
            "header_row_height": 3,
            "row_height": 3,
            "rows_per_page": 100,
            "sample_size": 500,
            "query": { "query": "service.name : *", "language": "kuery" },
            "filters": [],
            "data_source": {
              "type": "data_view_reference",
              "id": "REPLACE_DATA_VIEW_ID"
            },
            "view_mode": "patterns"
          }
        ]
      }
    }
  ]
}

@lukasolson lukasolson self-assigned this Feb 27, 2026
@lukasolson lukasolson added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting Team:DataDiscovery Discover, search (data plugin and KQL), data views, saved searches. For ES|QL, use Team:ES|QL. t// Project:Dashboards API labels Feb 27, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 5, 2026

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (6)
  • reviewer:coderabbit
  • Team:Search
  • Team:Operations
  • Team:QA
  • Team:SigEvents
  • Team:Kibana Management

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 08eeb149-f02b-474f-b70a-b65e0905b37c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

lukasolson added a commit to lukasolson/kibana that referenced this pull request Mar 12, 2026
… filter integration

- Fix type error in get_legacy_log_stream_embeddable_factory.ts: convert
  API-format sort/columns to stored format using toStoredSort/toStoredColumns
- Fix type error in get_search_embeddable_factory.tsx: replace stored-format
  comparator keys with API-format keys (snake_case) in initializeUnsavedChanges
- Fix toStoredSearchEmbeddableState to not include empty grid objects
- Add full round-trip tests (stored → API → stored) for by-value state
  including filters, query, columns, sort, grid, and row heights
- Add round-trip test for by-reference state with panel overrides
- Add filter-specific round-trip integration tests covering phrase, phrases,
  exists, range, DSL, and combined (group) filter types
- Update search_embeddable_transforms tests for new API-format expectations

Resolves remaining tasks from elastic#255213 for elastic#248927.

Made-with: Cursor
lukasolson added a commit to lukasolson/kibana that referenced this pull request Mar 12, 2026
… filter integration

- Fix type error in get_legacy_log_stream_embeddable_factory.ts: convert
  API-format sort/columns to stored format using toStoredSort/toStoredColumns
- Fix type error in get_search_embeddable_factory.tsx: replace stored-format
  comparator keys with API-format keys (snake_case) in initializeUnsavedChanges
- Fix toStoredSearchEmbeddableState to not include empty grid objects
- Add full round-trip tests (stored → API → stored) for by-value state
  including filters, query, columns, sort, grid, and row heights
- Add round-trip test for by-reference state with panel overrides
- Add filter-specific round-trip integration tests covering phrase, phrases,
  exists, range, DSL, and combined (group) filter types
- Update search_embeddable_transforms tests for new API-format expectations

Resolves remaining tasks from elastic#255213 for elastic#248927.

Made-with: Cursor
lukasolson and others added 10 commits March 13, 2026 12:18
…hema

- By-value: persist panel state in tabs[0] only. Transforms merge stored
  panel overrides into the single tab (stored→API) and read from tabs[0]
  when building stored state (API→stored). No top-level overrides.
- By-reference: persist panel overrides under `overrides`. Transforms
  read/write overrides instead of top-level sort, columns, row_height, etc.
  Map selected_tab_id ↔ selectedTabId in stored state.
- Deserialization: by-ref uses serializedState.overrides; by-value uses
  serializedState.tabs?.[0] for panel overrides.
- Comparators: restrict to DiscoverSessionEmbeddableState (overrides,
  selected_tab_id, discover_session_id, tabs). Use deepEquality for
  overrides and tabs so unsaved-changes badge works for both modes.
- Add overrides: {} when adding a panel from library (by-ref).
- Tests: add overrides to by-ref fixtures; put sort/columns in overrides;
  fix transformIn expected state (grid, selectedTabId); use API shape in
  by-value “references stored on dashboard” test.
Copy link
Copy Markdown
Contributor

@davismcphee davismcphee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code changes look great overall! I ended up leaving more feedback than I originally intended, but mainly questions and minor suggestions. I also know we're pretty short on time, so none of my feedback is really blocking.

I plan to test it a bunch tomorrow and will report back, but I'll leave the final dev review to @AlexGPlay since you're both closer to the as-code work.

Comment thread src/platform/plugins/shared/discover/common/embeddable/transform_utils.ts Outdated
Comment thread src/platform/plugins/shared/discover/common/embeddable/transform_utils.ts Outdated
Comment thread src/platform/plugins/shared/discover/common/embeddable/transform_utils.ts Outdated
{ meta: { id: 'discoverSessionEmbeddableDataTableSchema' } }
);

const panelOverridesSchema = schema.object(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not important for now, but do you think it would make sense to try and share definitions between the tab state and override state at some point (except maybe meta)?

Comment thread src/platform/plugins/shared/discover/server/embeddable/schema.ts Outdated
Comment thread src/platform/plugins/shared/discover/server/embeddable/schema.ts
Comment thread src/platform/plugins/shared/discover/server/embeddable/schema.ts Outdated
Comment on lines +71 to +83
let embeddableTransformsEnabled = false;
core
.getStartServices()
.then(([{ featureFlags }]) => {
featureFlags
.getBooleanValue$(EMBEDDABLE_TRANSFORMS_FEATURE_FLAG_KEY, embeddableTransformsEnabled)
.subscribe((value) => {
embeddableTransformsEnabled = value;
});
})
.catch((error) => {
this.logger.error(error);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case maybe we can just call featureFlags.getBooleanValue() directly where it's used below?

Copy link
Copy Markdown
Contributor

@davismcphee davismcphee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job on this! I tested it pretty thoroughly today and encountered almost no issues with the feature flag enabled or disabled. I left comments for the couple of things I noticed, but nothing that should block this PR imo.

This is what I did for testing:

  • Create and save a dashboard in main with various Discover panels
  • Switch to this branch, for both feature flag enabled and disabled:
    • Open the dashboard from main and confirm all Discover panels are correct
    • Use "save as" to save a copy of the dashboard
    • Recreate the same dashboard from scratch and save it
  • With the feature flag enabled, fetch all of the created dashboards from the API and diff them to confirm they are all identical apart from IDs
Copy of the dashboard I used for testing
{
    "options": {
      "hide_panel_titles": false,
      "hide_panel_borders": false,
      "use_margins": true,
      "auto_apply_filters": true,
      "sync_colors": false,
      "sync_cursor": true,
      "sync_tooltips": false
    },
    "query": {
      "query": "",
      "language": "kuery"
    },
    "time_range": {
      "from": "now-7d/d",
      "to": "now"
    },
    "title": "test",
    "panels": [
      {
        "grid": {
          "x": 0,
          "y": 0,
          "w": 24,
          "h": 15
        },
        "config": {
          "title": "ES|QL by ref",
          "description": "",
          "discover_session_id": "05479245-fab6-4950-905a-ebead2c16a4a",
          "selected_tab_id": "75dc317c-0fb1-4c43-ac85-0d3e64fb0675",
          "overrides": {}
        },
        "uid": "f0f23a51-ed74-4e42-a112-779fd0f8cce6",
        "type": "discover_session"
      },
      {
        "grid": {
          "x": 0,
          "y": 15,
          "w": 24,
          "h": 15
        },
        "config": {
          "title": "Classic by ref",
          "description": "",
          "discover_session_id": "05479245-fab6-4950-905a-ebead2c16a4a",
          "selected_tab_id": "7240f69c-462c-45ba-8a11-5cb484516a53",
          "overrides": {}
        },
        "uid": "7d00cbe9-1a0f-49e2-8a54-8924a2be7273",
        "type": "discover_session"
      },
      {
        "grid": {
          "x": 24,
          "y": 0,
          "w": 24,
          "h": 15
        },
        "config": {
          "title": "ES|QL by val",
          "tabs": [
            {
              "sort": [
                {
                  "name": "agent",
                  "direction": "asc"
                }
              ],
              "column_order": [
                "agent",
                "extension",
                "message"
              ],
              "header_row_height": 3,
              "density": "compact",
              "query": {
                "esql": "FROM kibana_sample_data_logs"
              }
            }
          ]
        },
        "uid": "60e16969-3da8-4781-827e-b7288224fff7",
        "type": "discover_session"
      },
      {
        "grid": {
          "x": 24,
          "y": 15,
          "w": 24,
          "h": 15
        },
        "config": {
          "title": "Classic by val",
          "tabs": [
            {
              "sort": [
                {
                  "name": "timestamp",
                  "direction": "desc"
                },
                {
                  "name": "agent.keyword",
                  "direction": "asc"
                }
              ],
              "column_order": [
                "agent.keyword",
                "extension",
                "message"
              ],
              "header_row_height": 3,
              "density": "compact",
              "query": {
                "query": "bytes > 500",
                "language": "kuery"
              },
              "filters": [],
              "data_source": {
                "type": "data_view_reference",
                "id": "90943e30-9a47-11e8-b64d-95841ca0b247"
              },
              "view_mode": "documents"
            }
          ]
        },
        "uid": "6970a0fd-5a44-4240-a421-63a689767173",
        "type": "discover_session"
      },
      {
        "grid": {
          "x": 0,
          "y": 30,
          "w": 24,
          "h": 15
        },
        "config": {
          "title": "ES|QL with overrides",
          "description": "",
          "discover_session_id": "05479245-fab6-4950-905a-ebead2c16a4a",
          "selected_tab_id": "75dc317c-0fb1-4c43-ac85-0d3e64fb0675",
          "overrides": {
            "sort": [],
            "column_order": [
              "message",
              "agent",
              "extension"
            ],
            "row_height": "auto"
          }
        },
        "uid": "ab861227-0e98-422d-b5a9-463adaef39aa",
        "type": "discover_session"
      },
      {
        "grid": {
          "x": 24,
          "y": 30,
          "w": 24,
          "h": 15
        },
        "config": {
          "title": "Classic with overrides",
          "description": "",
          "discover_session_id": "05479245-fab6-4950-905a-ebead2c16a4a",
          "selected_tab_id": "7240f69c-462c-45ba-8a11-5cb484516a53",
          "overrides": {
            "sort": [],
            "column_order": [
              "message",
              "agent.keyword",
              "extension"
            ]
          }
        },
        "uid": "f647cbc4-efdc-477e-907a-80096b973ef4",
        "type": "discover_session"
      }
    ],
    "pinned_panels": [],
    "access_control": {
      "access_mode": "default"
    }
  }

You can consider this approved on my end! But I'll leave the final official approval to @AlexGPlay.

Comment thread src/platform/plugins/shared/discover/common/embeddable/types.ts
Comment thread src/platform/plugins/shared/discover/server/plugin.ts
Comment thread src/platform/plugins/shared/discover/common/embeddable/transform_utils.ts Outdated
…hub.com:lukasolson/kibana into discover_sessions_as_code/embeddable_transforms
Comment on lines +178 to +210
export function fromStoredTab(
tab: DiscoverSessionTabAttributes,
references: SavedObjectReference[] = []
): DiscoverSessionTab {
const {
sort,
sampleSize,
rowsPerPage,
headerRowHeight,
density,
viewMode,
kibanaSavedObjectMeta: { searchSourceJSON },
} = tab;
const apiTab = {
...toDiscoverSessionPanelOverrides(tab),
sort: fromStoredSort(sort),
header_row_height: fromStoredHeight(headerRowHeight),
density: density ?? DataGridDensity.COMPACT,
};
const searchSourceValues = parseSearchSourceJSON(searchSourceJSON);
const { index, query, filter } = injectReferences(searchSourceValues, references);
return isOfAggregateQueryType(query)
? { ...apiTab, query }
: {
...apiTab,
...(sampleSize && { sample_size: sampleSize }),
...(rowsPerPage && { rows_per_page: rowsPerPage }),
query,
filters: fromStoredFilters(filter) ?? [],
data_source: fromStoredDataView(index),
view_mode: viewMode ?? VIEW_MODE.DOCUMENT_LEVEL,
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low embeddable/transform_utils.ts:178

fromStoredTab calls fromStoredSort(sort) without checking if sort is defined, and fromStoredSort immediately calls sort.map(). When tab.sort is undefined, this throws TypeError: Cannot read properties of undefined (reading 'map'). Other call sites guard this with sort && checks, confirming the field is optional. Consider adding a null check before invoking fromStoredSort.

     const apiTab = {
     ...toDiscoverSessionPanelOverrides(tab),
-    sort: fromStoredSort(sort),
+    sort: sort ? fromStoredSort(sort) : [],
     header_row_height: fromStoredHeight(headerRowHeight),
     density: density ?? DataGridDensity.COMPACT,
   };
🤖 Copy this AI Prompt to have your agent fix this:
In file src/platform/plugins/shared/discover/common/embeddable/transform_utils.ts around lines 178-210:

`fromStoredTab` calls `fromStoredSort(sort)` without checking if `sort` is defined, and `fromStoredSort` immediately calls `sort.map()`. When `tab.sort` is undefined, this throws `TypeError: Cannot read properties of undefined (reading 'map')`. Other call sites guard this with `sort &&` checks, confirming the field is optional. Consider adding a null check before invoking `fromStoredSort`.

Evidence trail:
src/platform/plugins/shared/discover/common/embeddable/transform_utils.ts lines 178-193 (fromStoredTab calling fromStoredSort without guard), line 242 (guarded call with `sort &&`), lines 291-299 (fromStoredSort definition calling sort.map() directly). Schema at src/platform/plugins/shared/saved_search/server/saved_objects/schema.ts lines 146-148 (DISCOVER_SESSION_TAB_ATTRIBUTES definition), line 170 showing `sort: schema.maybe(sort)` in VERSION_7.

Copy link
Copy Markdown
Contributor

@iblancof iblancof left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code changes in src/platform/packages/shared/kbn-saved-search-component/src/components/saved_search.tsx LGTM

@kertal kertal requested a review from nreese April 1, 2026 07:42
Copy link
Copy Markdown
Contributor

@AlexGPlay AlexGPlay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm - there are a lot of things going on so hopefully with Davis taking a look too nothing escaped the review 🧐

@elasticmachine
Copy link
Copy Markdown
Contributor

elasticmachine commented Apr 1, 2026

💔 Build Failed

Failed CI Steps

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
discover 1959 1972 +13

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/as-code-data-views-schema 2 8 +6
discover 150 158 +8
total +14

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
discover 1.6MB 1.6MB +8.3KB

Public APIs missing exports

Total count of every type that is part of your API that should be exported but is not. This will cause broken links in the API documentation system. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats exports for more detailed information.

id before after diff
discover 22 23 +1

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
discover 27.6KB 26.9KB -629.0B
Unknown metric groups

API count

id before after diff
@kbn/as-code-data-views-schema 2 10 +8
@kbn/as-code-data-views-transforms 10 14 +4
discover 206 215 +9
total +21

async chunk count

id before after diff
discover 43 45 +2

History

cc @lukasolson

@lukasolson lukasolson merged commit 582946a into elastic:main Apr 2, 2026
19 checks passed
paulinashakirova pushed a commit to paulinashakirova/kibana that referenced this pull request Apr 2, 2026
…ransforms, and feature flag (elastic#255213)

## Summary

Resolves elastic#240164.
Resolves elastic#248926.
Resolves elastic#248927.

Implements **Discover Session search embeddable** support for the
Dashboards / embeddables “as code” direction: bidirectional conversion
between **persisted Saved Search panel state** (legacy `savedObjectId` /
`attributes.tabs` shape) and a **simplified API schema**
(`discover_session_id`, `selected_tab_id`, `tabs`, `overrides`,
snake_case panel fields, as-code filters and data sources). Registers a
**config-schema** for that API shape (when the feature flag is on) so
server-side validation and OpenAPI-style docs can describe by-value vs
by-reference panels clearly.

---

## Feature flag

- **`discover.embeddableTransforms`**
(`EMBEDDABLE_TRANSFORMS_FEATURE_FLAG_KEY` in
`discover/common/constants.ts`)
- **When off (default):** behavior matches pre-PR paths: `transformOut`
uses the legacy panel shape; `transformIn` only runs the legacy Saved
Search pipeline; **no** embeddable schema is registered from Discover
server; client serialization keeps the legacy shape.
- **When on:** `transformOut` emits the simplified Discover Session
embeddable state; `transformIn` accepts **either** legacy panel state
**or** API state and normalizes to stored shape; **Discover server**
supplies `getDiscoverSessionEmbeddableSchema`; **client**
`serializeState` / `deserializeState` read and write the API types
end-to-end for the running embeddable.

---

## Transforms (common)

Central logic lives in `discover/common/embeddable/`:

| Direction | Role |
|-----------|------|
| **Stored → API** | `savedSearchToDiscoverSessionEmbeddableState` (+
`byValue*` / `byReference*` helpers) used from `getTransformOut` when
the flag is enabled. |
| **API → stored** | `discoverSessionToSavedSearchEmbeddableState` (+
helpers) used from `getTransformIn` when input is already API-shaped. |
| **Legacy path** | `legacyTransformIn` / `legacyTransformOut` preserve
previous behavior for BWC; `SearchEmbeddablePanelApiState` is a
**union** of legacy and API state (`types.ts`). |

**Type guards** (`type_guards.ts`) distinguish legacy vs API panel state
so drilldown transforms and the rest of the pipeline stay correct.

**Integration with as-code packages:**

- Filters: `fromStoredFilters` / `toStoredFilters`
(`@kbn/as-code-filters-transforms`).
- Data views / data source: `fromStoredDataView` / `toStoredDataView`
(`@kbn/as-code-data-views-transforms`) with a **shared** discriminated
data source schema (reference vs inline spec, optional **runtime
fields** via `schema_runtime_field`).

---

## By-value vs by-reference semantics

- **By-value API:** single-tab model exposed as `tabs` (length 1 today).
Panel-level table options merge with that tab on **stored → API**; **API
→ stored** and client deserialize rebuild Saved Search attributes from
`tabs[0]` plus overrides.
- **By-reference API:** `discover_session_id`, optional
`selected_tab_id`, and **`overrides`** for dashboard-level edits (sort,
columns, grid, row heights, sample size, etc.) instead of duplicating
those at the top level of the API object.
- **Client runtime:** `deserializeState` maps API fields
(`discover_session_id` → `savedObjectId`, etc.), applies `overrides`
into the runtime Saved Search shape, and keeps title / time range /
drilldowns behavior aligned with the publishing layer.

---

## Server: schema registration

- `discover/server/embeddable/schema.ts` defines **classic** vs
**ES\|QL** tab schemas (`query` + `filters` + `data_source` +
`view_mode` vs `aggregateQuerySchema`), panel overrides, drilldown
intersection with `serializedTitlesSchema`, `serializedTimeRangeSchema`,
and drilldown schema from the embeddable host.
- **By-value / by-reference** branches use `BY_VALUE_SCHEMA_META` and
`BY_REF_SCHEMA_META` from `@kbn/presentation-publishing-schemas` so
`oneOf` / “Any of” documentation shows human-readable option labels.
- `DiscoverServerPlugin` registers `getSchema` only when the feature
flag is enabled (avoids registering API schema when the feature is off).

Exported **TypeOf** types from that schema are the source of truth for
`DiscoverSessionEmbeddable*` types consumed by transforms and the public
embeddable factory typing.

---

## Client embeddable

Previously, by-value Discover search panels could fail to save
dashboard-level changes reliably: what we wrote back into the panel JSON
didn’t line up with how the embeddable deserialized and compared state,
so edits didn’t round-trip cleanly.

With `discover.embeddableTransforms` enabled, serialize / deserialize
use the same Discover Session API shape as `transformIn` /
`transformOut`: by-value state is anchored on `tabs[0]` (with panel
fields merged consistently), and serialization emits that shape instead
of the older Saved Search–only panel blob. That alignment fixes
save-and-return and normal dashboard save for by-value panels so changes
actually stick.

---

## Example API calls

```txt
---------------------------------------------------------------------------
By reference — minimal (first tab, no overrides)
---------------------------------------------------------------------------
POST kbn:/api/dashboards?apiVersion=1
{
  "title": "Minimal by-ref Discover panel",
  "panels": [
    {
      "grid": { "x": 0, "y": 0, "w": 48, "h": 15 },
      "type": "discover_session",
      "config": {
        "title": "Saved session",
        "discover_session_id": "REPLACE_WITH_SEARCH_SAVED_OBJECT_ID"
      }
    }
  ]
}

---------------------------------------------------------------------------
By reference — overrides only in API shape (sort, columns, pagination, auto heights)
---------------------------------------------------------------------------
# `overrides` can include: column_order, column_settings, sort, density, header_row_height,
# row_height (numbers or "auto"), rows_per_page (10|25|50|100|250|500), sample_size (10–10000).

POST kbn:/api/dashboards?apiVersion=1
{
  "title": "By-ref with rich overrides",
  "panels": [
    {
      "grid": { "x": 0, "y": 0, "w": 48, "h": 18 },
      "type": "discover_session",
      "config": {
        "title": "Session + dashboard-specific table",
        "discover_session_id": "REPLACE_WITH_SEARCH_SAVED_OBJECT_ID",
        "selected_tab_id": "REPLACE_TAB_ID_OPTIONAL",
        "overrides": {
          "sort": [{ "name": "@timestamp", "direction": "desc" }],
          "column_order": ["message", "@timestamp", "host.name"],
          "column_settings": { "message": { "width": 420 } },
          "density": "compact",
          "header_row_height": "auto",
          "row_height": "auto",
          "rows_per_page": 50,
          "sample_size": 500
        }
      }
    }
  ]
}

---------------------------------------------------------------------------
By value — classic tab with data view reference + view modes (documents / patterns / aggregated)
---------------------------------------------------------------------------
# view_mode: "documents" | "patterns" | "aggregated"

POST kbn:/api/dashboards?apiVersion=1
{
  "title": "Inline Discover — patterns mode",
  "panels": [
    {
      "grid": { "x": 0, "y": 0, "w": 48, "h": 15 },
      "type": "discover_session",
      "config": {
        "title": "Patterns (by value)",
        "tabs": [
          {
            "sort": [],
            "column_order": [],
            "density": "normal",
            "header_row_height": 3,
            "row_height": 3,
            "rows_per_page": 100,
            "sample_size": 500,
            "query": { "query": "service.name : *", "language": "kuery" },
            "filters": [],
            "data_source": {
              "type": "data_view_reference",
              "id": "REPLACE_DATA_VIEW_ID"
            },
            "view_mode": "patterns"
          }
        ]
      }
    }
  ]
}
```

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
iblancof added a commit that referenced this pull request Apr 20, 2026
…ialization (#261558)

## Summary

Relates to #261234

`nonHighlightingFilters` was silently dropped during `fromStoredTab` /
`toStoredTab` serialization in the Discover session embeddable as part
of [[Discover Sessions as Code] Discover search embeddable: API schema,
transforms, and feature
flag](#255213). This caused the
logs section in the traces flyout to run without the trace context
filter (`trace.id`, `transaction.id`, `span.id`), returning all logs in
the index instead of only the ones correlated to the opened trace
document.

|Before|After|
|-|-|
|<img width="1496" height="965" alt="Screenshot 2026-04-07 at 15 05 34"
src="https://github.com/user-attachments/assets/9a1f21cd-3359-4753-b7d6-3bf1e3cef758"
/>|<img width="1495" height="965" alt="Screenshot 2026-04-07 at 15 03
34"
src="https://github.com/user-attachments/assets/87fb9b18-6bef-49dd-94ec-a3eea1d895f8"
/>|

`nonHighlightingFilters` was introduced in [[One Discover] [Logs UX]Add
support to disable highlighting for certain filters we deem
internal](#239402) to allow
certain filters (trace context constraints) to be applied to the ES
query without affecting result highlighting. The `fromStoredTab` /
`toStoredTab` serialization round-trip was added later in [[Discover
Sessions as Code] Discover search embeddable: API schema, transforms,
and feature flag](#255213) and
never included this field.

~~The fix ensures `nonHighlightingFilters` is read back from the
serialized search source in `fromStoredTab` and written into
`toStoredTab`, and adds the corresponding field to the server-side
schema.~~

The fix avoids exposing `nonHighlightingFilters` as a public API schema
field entirely. Since trace context filters are always runtime-derived
from the opened document (`traceId`, `spanId`, `transactionId`), they
never need to be persisted in the serialized state.

The runtime prop path, `nonHighlightingQuery` →
`createLogEventsRenderer` → `nonHighlightingFilters` on the initial
searchSource, already handles the use case correctly on every mount.

A `syncNonHighlightingFilters` effect has also been added to
`SavedSearchComponentTable` as a defensive measure, consistent with how
other dynamic props (`filters`, `query`, `timeRange`) are synced after
mount.

A defensive e2e test has been added: it opens the minimal trace flyout
(2 correlated logs) and asserts the logs section shows exactly 2
results.
kibanamachine pushed a commit to kibanamachine/kibana that referenced this pull request Apr 20, 2026
…ialization (elastic#261558)

## Summary

Relates to elastic#261234

`nonHighlightingFilters` was silently dropped during `fromStoredTab` /
`toStoredTab` serialization in the Discover session embeddable as part
of [[Discover Sessions as Code] Discover search embeddable: API schema,
transforms, and feature
flag](elastic#255213). This caused the
logs section in the traces flyout to run without the trace context
filter (`trace.id`, `transaction.id`, `span.id`), returning all logs in
the index instead of only the ones correlated to the opened trace
document.

|Before|After|
|-|-|
|<img width="1496" height="965" alt="Screenshot 2026-04-07 at 15 05 34"
src="https://github.com/user-attachments/assets/9a1f21cd-3359-4753-b7d6-3bf1e3cef758"
/>|<img width="1495" height="965" alt="Screenshot 2026-04-07 at 15 03
34"
src="https://github.com/user-attachments/assets/87fb9b18-6bef-49dd-94ec-a3eea1d895f8"
/>|

`nonHighlightingFilters` was introduced in [[One Discover] [Logs UX]Add
support to disable highlighting for certain filters we deem
internal](elastic#239402) to allow
certain filters (trace context constraints) to be applied to the ES
query without affecting result highlighting. The `fromStoredTab` /
`toStoredTab` serialization round-trip was added later in [[Discover
Sessions as Code] Discover search embeddable: API schema, transforms,
and feature flag](elastic#255213) and
never included this field.

~~The fix ensures `nonHighlightingFilters` is read back from the
serialized search source in `fromStoredTab` and written into
`toStoredTab`, and adds the corresponding field to the server-side
schema.~~

The fix avoids exposing `nonHighlightingFilters` as a public API schema
field entirely. Since trace context filters are always runtime-derived
from the opened document (`traceId`, `spanId`, `transactionId`), they
never need to be persisted in the serialized state.

The runtime prop path, `nonHighlightingQuery` →
`createLogEventsRenderer` → `nonHighlightingFilters` on the initial
searchSource, already handles the use case correctly on every mount.

A `syncNonHighlightingFilters` effect has also been added to
`SavedSearchComponentTable` as a defensive measure, consistent with how
other dynamic props (`filters`, `query`, `timeRange`) are synced after
mount.

A defensive e2e test has been added: it opens the minimal trace flyout
(2 correlated logs) and asserts the logs section shows exactly 2
results.

(cherry picked from commit ca6f9ec)
kibanamachine added a commit that referenced this pull request Apr 21, 2026
…ab serialization (#261558) (#264345)

# Backport

This will backport the following commits from `main` to `9.4`:
- [[Discover][Traces] Fix nonHighlightingFilters lost in session tab
serialization (#261558)](#261558)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Irene
Blanco","email":"irene.blanco@elastic.co"},"sourceCommit":{"committedDate":"2026-04-20T09:04:45Z","message":"[Discover][Traces]
Fix nonHighlightingFilters lost in session tab serialization
(#261558)\n\n## Summary\n\nRelates to
https://github.com/elastic/kibana/issues/261234\n\n`nonHighlightingFilters`
was silently dropped during `fromStoredTab` /\n`toStoredTab`
serialization in the Discover session embeddable as part\nof [[Discover
Sessions as Code] Discover search embeddable: API schema,\ntransforms,
and feature\nflag](#255213). This
caused the\nlogs section in the traces flyout to run without the trace
context\nfilter (`trace.id`, `transaction.id`, `span.id`), returning all
logs in\nthe index instead of only the ones correlated to the opened
trace\ndocument.\n\n|Before|After|\n|-|-|\n|<img width=\"1496\"
height=\"965\" alt=\"Screenshot 2026-04-07 at 15 05
34\"\nsrc=\"https://github.com/user-attachments/assets/9a1f21cd-3359-4753-b7d6-3bf1e3cef758\"\n/>|<img
width=\"1495\" height=\"965\" alt=\"Screenshot 2026-04-07 at 15
03\n34\"\nsrc=\"https://github.com/user-attachments/assets/87fb9b18-6bef-49dd-94ec-a3eea1d895f8\"\n/>|\n\n`nonHighlightingFilters`
was introduced in [[One Discover] [Logs UX]Add\nsupport to disable
highlighting for certain filters we
deem\ninternal](#239402) to
allow\ncertain filters (trace context constraints) to be applied to the
ES\nquery without affecting result highlighting. The `fromStoredTab`
/\n`toStoredTab` serialization round-trip was added later in
[[Discover\nSessions as Code] Discover search embeddable: API schema,
transforms,\nand feature
flag](#255213) and\nnever included
this field.\n\n~~The fix ensures `nonHighlightingFilters` is read back
from the\nserialized search source in `fromStoredTab` and written
into\n`toStoredTab`, and adds the corresponding field to the
server-side\nschema.~~\n\nThe fix avoids exposing
`nonHighlightingFilters` as a public API schema\nfield entirely. Since
trace context filters are always runtime-derived\nfrom the opened
document (`traceId`, `spanId`, `transactionId`), they\nnever need to be
persisted in the serialized state.\n\nThe runtime prop path,
`nonHighlightingQuery` →\n`createLogEventsRenderer` →
`nonHighlightingFilters` on the initial\nsearchSource, already handles
the use case correctly on every mount.\n\nA `syncNonHighlightingFilters`
effect has also been added to\n`SavedSearchComponentTable` as a
defensive measure, consistent with how\nother dynamic props (`filters`,
`query`, `timeRange`) are synced after\nmount.\n\nA defensive e2e test
has been added: it opens the minimal trace flyout\n(2 correlated logs)
and asserts the logs section shows exactly
2\nresults.","sha":"ca6f9ec3b2ab13572f5fe7144537cb232870f930","branchLabelMapping":{"^v9.5.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","backport:version","Feature:Traces
in
Discover","Team:obs-exploration","v9.4.0","v9.5.0"],"title":"[Discover][Traces]
Fix nonHighlightingFilters lost in session tab
serialization","number":261558,"url":"https://github.com/elastic/kibana/pull/261558","mergeCommit":{"message":"[Discover][Traces]
Fix nonHighlightingFilters lost in session tab serialization
(#261558)\n\n## Summary\n\nRelates to
https://github.com/elastic/kibana/issues/261234\n\n`nonHighlightingFilters`
was silently dropped during `fromStoredTab` /\n`toStoredTab`
serialization in the Discover session embeddable as part\nof [[Discover
Sessions as Code] Discover search embeddable: API schema,\ntransforms,
and feature\nflag](#255213). This
caused the\nlogs section in the traces flyout to run without the trace
context\nfilter (`trace.id`, `transaction.id`, `span.id`), returning all
logs in\nthe index instead of only the ones correlated to the opened
trace\ndocument.\n\n|Before|After|\n|-|-|\n|<img width=\"1496\"
height=\"965\" alt=\"Screenshot 2026-04-07 at 15 05
34\"\nsrc=\"https://github.com/user-attachments/assets/9a1f21cd-3359-4753-b7d6-3bf1e3cef758\"\n/>|<img
width=\"1495\" height=\"965\" alt=\"Screenshot 2026-04-07 at 15
03\n34\"\nsrc=\"https://github.com/user-attachments/assets/87fb9b18-6bef-49dd-94ec-a3eea1d895f8\"\n/>|\n\n`nonHighlightingFilters`
was introduced in [[One Discover] [Logs UX]Add\nsupport to disable
highlighting for certain filters we
deem\ninternal](#239402) to
allow\ncertain filters (trace context constraints) to be applied to the
ES\nquery without affecting result highlighting. The `fromStoredTab`
/\n`toStoredTab` serialization round-trip was added later in
[[Discover\nSessions as Code] Discover search embeddable: API schema,
transforms,\nand feature
flag](#255213) and\nnever included
this field.\n\n~~The fix ensures `nonHighlightingFilters` is read back
from the\nserialized search source in `fromStoredTab` and written
into\n`toStoredTab`, and adds the corresponding field to the
server-side\nschema.~~\n\nThe fix avoids exposing
`nonHighlightingFilters` as a public API schema\nfield entirely. Since
trace context filters are always runtime-derived\nfrom the opened
document (`traceId`, `spanId`, `transactionId`), they\nnever need to be
persisted in the serialized state.\n\nThe runtime prop path,
`nonHighlightingQuery` →\n`createLogEventsRenderer` →
`nonHighlightingFilters` on the initial\nsearchSource, already handles
the use case correctly on every mount.\n\nA `syncNonHighlightingFilters`
effect has also been added to\n`SavedSearchComponentTable` as a
defensive measure, consistent with how\nother dynamic props (`filters`,
`query`, `timeRange`) are synced after\nmount.\n\nA defensive e2e test
has been added: it opens the minimal trace flyout\n(2 correlated logs)
and asserts the logs section shows exactly
2\nresults.","sha":"ca6f9ec3b2ab13572f5fe7144537cb232870f930"}},"sourceBranch":"main","suggestedTargetBranches":["9.4"],"targetPullRequestStates":[{"branch":"9.4","label":"v9.4.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.5.0","branchLabelMappingKey":"^v9.5.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/261558","number":261558,"mergeCommit":{"message":"[Discover][Traces]
Fix nonHighlightingFilters lost in session tab serialization
(#261558)\n\n## Summary\n\nRelates to
https://github.com/elastic/kibana/issues/261234\n\n`nonHighlightingFilters`
was silently dropped during `fromStoredTab` /\n`toStoredTab`
serialization in the Discover session embeddable as part\nof [[Discover
Sessions as Code] Discover search embeddable: API schema,\ntransforms,
and feature\nflag](#255213). This
caused the\nlogs section in the traces flyout to run without the trace
context\nfilter (`trace.id`, `transaction.id`, `span.id`), returning all
logs in\nthe index instead of only the ones correlated to the opened
trace\ndocument.\n\n|Before|After|\n|-|-|\n|<img width=\"1496\"
height=\"965\" alt=\"Screenshot 2026-04-07 at 15 05
34\"\nsrc=\"https://github.com/user-attachments/assets/9a1f21cd-3359-4753-b7d6-3bf1e3cef758\"\n/>|<img
width=\"1495\" height=\"965\" alt=\"Screenshot 2026-04-07 at 15
03\n34\"\nsrc=\"https://github.com/user-attachments/assets/87fb9b18-6bef-49dd-94ec-a3eea1d895f8\"\n/>|\n\n`nonHighlightingFilters`
was introduced in [[One Discover] [Logs UX]Add\nsupport to disable
highlighting for certain filters we
deem\ninternal](#239402) to
allow\ncertain filters (trace context constraints) to be applied to the
ES\nquery without affecting result highlighting. The `fromStoredTab`
/\n`toStoredTab` serialization round-trip was added later in
[[Discover\nSessions as Code] Discover search embeddable: API schema,
transforms,\nand feature
flag](#255213) and\nnever included
this field.\n\n~~The fix ensures `nonHighlightingFilters` is read back
from the\nserialized search source in `fromStoredTab` and written
into\n`toStoredTab`, and adds the corresponding field to the
server-side\nschema.~~\n\nThe fix avoids exposing
`nonHighlightingFilters` as a public API schema\nfield entirely. Since
trace context filters are always runtime-derived\nfrom the opened
document (`traceId`, `spanId`, `transactionId`), they\nnever need to be
persisted in the serialized state.\n\nThe runtime prop path,
`nonHighlightingQuery` →\n`createLogEventsRenderer` →
`nonHighlightingFilters` on the initial\nsearchSource, already handles
the use case correctly on every mount.\n\nA `syncNonHighlightingFilters`
effect has also been added to\n`SavedSearchComponentTable` as a
defensive measure, consistent with how\nother dynamic props (`filters`,
`query`, `timeRange`) are synced after\nmount.\n\nA defensive e2e test
has been added: it opens the minimal trace flyout\n(2 correlated logs)
and asserts the logs section shows exactly
2\nresults.","sha":"ca6f9ec3b2ab13572f5fe7144537cb232870f930"}}]}]
BACKPORT-->

Co-authored-by: Irene Blanco <irene.blanco@elastic.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting Project:Dashboards API release_note:skip Skip the PR/issue when compiling release notes Team:DataDiscovery Discover, search (data plugin and KQL), data views, saved searches. For ES|QL, use Team:ES|QL. t// v9.4.0

Projects

None yet

9 participants