Skip to content

[CPS] Simplify project_routing propagation in the data plugin via x-kbn-project-routing header#4

Closed
gsoldevila wants to merge 3 commits intocps/kbn-project-routing-headerfrom
cps/data-plugin-header-routing
Closed

[CPS] Simplify project_routing propagation in the data plugin via x-kbn-project-routing header#4
gsoldevila wants to merge 3 commits intocps/kbn-project-routing-headerfrom
cps/data-plugin-header-routing

Conversation

@gsoldevila
Copy link
Copy Markdown
Owner

Summary

Proof of concept that simplifies `project_routing` propagation in the `data` plugin by leveraging the `x-kbn-project-routing` HTTP header mechanism introduced in elastic#256839.

Closes elastic/kibana-team#3032

Changes

Client side (`search_interceptor.ts`): Instead of including `projectRouting` in the JSON body, the search interceptor now injects `x-kbn-project-routing` as an HTTP header when a project routing value is resolved from the CPS manager. The `x-kbn-` prefix is required to avoid the browser-side HTTP client validation that rejects `kbn-` headers.

Server side:

  • `search.ts` route: `projectRouting` removed from the body schema and handler — the header travels transparently to the ES client.
  • `search_service.ts`: `createScopedEsClient` now defaults to `{ projectRouting: 'request-header' }` instead of the bare `client.asScoped(request)` (origin-only). When the header is absent (non-CPS context), the handler falls back to origin-only routing, preserving existing behaviour.
  • `async_utils.ts` / `es_search_strategy.ts`: Manual `project_routing` injection removed from strategy-level ES request params — routing is now handled entirely at the transport level by the scoped client.

ESQL async search strategy (`esql_async_search_strategy.ts`): Converted from raw `transport.request()` calls to typed `esql.asyncQuery()` / `asyncQueryGet()` / `asyncQueryStop()` / `asyncQueryDelete()` client methods, inspired by elastic#256456. Typed methods include `meta.acceptedParams` so the CPS `OnRequestHandler` automatically injects `project_routing` without manual propagation. Also fixes the pre-existing incorrect `SqlGetAsyncResponse` type to `EsqlAsyncEsqlResult`.

Types (`kbn-search-types`): `projectRouting` removed from `ISearchOptionsSerializable` since it is no longer serialised into the request body.

`kbn-cps-utils`: `KBN_PROJECT_ROUTING_HEADER` re-exported so browser-side code can reference the header constant without depending on a server-only package.

Test plan

  • All existing unit tests updated and passing.
  • `yarn test:jest` on `search_interceptor.test.ts`, `search_service.test.ts`, `async_utils.test.ts`, `es_search_strategy.test.ts`, `search.test.ts`, `esql_async_search_strategy.test.ts`.
  • `yarn test:type_check --project src/platform/plugins/shared/data/tsconfig.json` — zero errors.
  • Manually tested with CPS enabled: both `/internal/search/esql_async` (ESQL) and `/internal/search/ese` (classic KQL) correctly filter results based on the project picker selection.

Made with Cursor

@gsoldevila gsoldevila force-pushed the cps/kbn-project-routing-header branch from a01d506 to edbe973 Compare March 12, 2026 13:51
@gsoldevila gsoldevila force-pushed the cps/data-plugin-header-routing branch 2 times, most recently from 453aec5 to 054072a Compare March 12, 2026 16:39
…-project-routing header

Replaces the manual propagation of `projectRouting` through the data plugin's search
request body (client → route handler → strategy → ES body) with the header-based
mechanism introduced in the companion PR.

On the client side, the search interceptor now injects `kbn-project-routing` as an HTTP
request header instead of including `projectRouting` in the serialised body. On the server
side, `createScopedEsClient` defaults to `{ projectRouting: 'request-header' }` so that
the core CPS handler reads the header and injects `project_routing` at the transport level.
Strategies no longer need to manually inject the value into each Elasticsearch request body.

Closes elastic/kibana-team#3032

Made-with: Cursor
Replace transport.request() calls with typed elasticsearch-js client
methods:
- POST /_query/async       -> esClient.esql.asyncQuery()
- GET /_query/async/${id}  -> esClient.esql.asyncQueryGet()
- POST /_query/async/${id}/stop -> esClient.esql.asyncQueryStop()
- DELETE /_query/async/${id}    -> esClient.esql.asyncQueryDelete()

Typed methods include meta.acceptedParams so the CPS OnRequestHandler
automatically injects project_routing — no explicit injection needed.
Also fixes the incorrect SqlGetAsyncResponse type to EsqlAsyncEsqlResult.

Inspired by elastic#256456.

Made-with: Cursor
… data view editor

When creating a data view, the preview panel has two modes:
- "All sources": should show every index across all connected CPS projects
- "Matching sources": should respect the current project-picker selection

Previously getIndicesCached used projectRouting only as a cache-key
component but never passed it to dataViews.getIndices(), so both
views fetched from the same CPS-scoped set of indices.

This commit threads a projectRouting override through getIndicesCached
and into dataViews.getIndices(). loadIndices now passes
PROJECT_ROUTING.ALL when CPS is active so the 'pattern: *' fetch
spans every connected project, giving an accurate "All sources" count.

Made-with: Cursor
@gsoldevila gsoldevila force-pushed the cps/data-plugin-header-routing branch from 054072a to a9a71ac Compare March 13, 2026 08:45
@gsoldevila gsoldevila closed this Mar 13, 2026
gsoldevila pushed a commit that referenced this pull request Mar 27, 2026
Closes elastic#258317

## Summary

The alert episodes table needs to display episode status for each row.

To build that UI, we needed a bulk-get API for alert actions.

### Testing

<details> <summary>Start by posting some mock action data.</summary>

```
POST .alerting-actions/_bulk
{"create":{}}
{"@timestamp":"2026-03-18T08:00:00.000Z","last_series_event_timestamp":"2026-03-18T07:55:00.000Z","actor":"user-1","action_type":"ack","group_hash":"gh-1","episode_id":"ep-001","rule_id":"rule-1"}
{"create":{}}
{"@timestamp":"2026-03-18T08:10:00.000Z","last_series_event_timestamp":"2026-03-18T07:55:00.000Z","actor":"user-1","action_type":"snooze","group_hash":"gh-1","episode_id":"ep-001","rule_id":"rule-1"}
{"create":{}}
{"@timestamp":"2026-03-18T08:30:00.000Z","last_series_event_timestamp":"2026-03-18T07:55:00.000Z","actor":"user-2","action_type":"deactivate","group_hash":"gh-2","episode_id":"ep-002","rule_id":"rule-1","reason":"Known maintenance window"}
{"create":{}}
{"@timestamp":"2026-03-18T08:45:00.000Z","last_series_event_timestamp":"2026-03-18T07:55:00.000Z","actor":"user-2","action_type":"ack","group_hash":"gh-2","episode_id":"ep-002","rule_id":"rule-1"}
{"create":{}}
{"@timestamp":"2026-03-18T09:00:00.000Z","last_series_event_timestamp":"2026-03-18T08:50:00.000Z","actor":"user-1","action_type":"ack","group_hash":"gh-3","episode_id":"ep-003","rule_id":"rule-2"}
{"create":{}}
{"@timestamp":"2026-03-18T09:15:00.000Z","last_series_event_timestamp":"2026-03-18T08:50:00.000Z","actor":"user-1","action_type":"unack","group_hash":"gh-3","episode_id":"ep-003","rule_id":"rule-2"}
{"create":{}}
{"@timestamp":"2026-03-18T09:30:00.000Z","last_series_event_timestamp":"2026-03-18T09:20:00.000Z","actor":"user-3","action_type":"snooze","group_hash":"gh-4","episode_id":"ep-004","rule_id":"rule-2"}
{"create":{}}
{"@timestamp":"2026-03-18T09:50:00.000Z","last_series_event_timestamp":"2026-03-18T09:20:00.000Z","actor":"user-3","action_type":"unsnooze","group_hash":"gh-4","episode_id":"ep-004","rule_id":"rule-2"}
{"create":{}}
{"@timestamp":"2026-03-18T10:00:00.000Z","last_series_event_timestamp":"2026-03-18T09:50:00.000Z","actor":"user-2","action_type":"deactivate","group_hash":"gh-5","episode_id":"ep-005","rule_id":"rule-3","reason":"Duplicate alert"}
{"create":{}}
{"@timestamp":"2026-03-18T10:20:00.000Z","last_series_event_timestamp":"2026-03-18T09:50:00.000Z","actor":"user-1","action_type":"activate","group_hash":"gh-5","episode_id":"ep-005","rule_id":"rule-3","reason":"Re-enabled after investigation"}
{"create":{}}
{"@timestamp":"2026-03-18T10:30:00.000Z","last_series_event_timestamp":"2026-03-18T10:25:00.000Z","actor":"user-1","action_type":"ack","group_hash":"elasticgh-6","episode_id":"ep-006","rule_id":"rule-3"}
{"create":{}}
{"@timestamp":"2026-03-18T10:45:00.000Z","last_series_event_timestamp":"2026-03-18T10:25:00.000Z","actor":"user-1","action_type":"snooze","group_hash":"elasticgh-6","episode_id":"ep-006","rule_id":"rule-3"}
{"create":{}}
{"@timestamp":"2026-03-18T10:55:00.000Z","last_series_event_timestamp":"2026-03-18T10:25:00.000Z","actor":"user-2","action_type":"deactivate","group_hash":"elasticgh-6","episode_id":"ep-006","rule_id":"rule-3","reason":"Root cause fixed"}
{"create":{}}
{"@timestamp":"2026-03-18T11:00:00.000Z","last_series_event_timestamp":"2026-03-18T10:55:00.000Z","actor":"user-3","action_type":"ack","group_hash":"elasticgh-7","episode_id":"ep-007","rule_id":"rule-4"}
{"create":{}}
{"@timestamp":"2026-03-18T11:30:00.000Z","last_series_event_timestamp":"2026-03-18T11:20:00.000Z","actor":"user-2","action_type":"snooze","group_hash":"elasticgh-8","episode_id":"ep-008","rule_id":"rule-4"}
{"create":{}}
{"@timestamp":"2026-03-18T11:45:00.000Z","last_series_event_timestamp":"2026-03-18T11:20:00.000Z","actor":"user-2","action_type":"ack","group_hash":"elasticgh-8","episode_id":"ep-008","rule_id":"rule-4"}
{"create":{}}
{"@timestamp":"2026-03-18T12:00:00.000Z","last_series_event_timestamp":"2026-03-18T11:50:00.000Z","actor":"user-1","action_type":"deactivate","group_hash":"elasticgh-9","episode_id":"ep-009","rule_id":"rule-5","reason":"Alert storm - suppressing"}
{"create":{}}
{"@timestamp":"2026-03-18T12:10:00.000Z","last_series_event_timestamp":"2026-03-18T11:50:00.000Z","actor":"user-1","action_type":"snooze","group_hash":"elasticgh-9","episode_id":"ep-009","rule_id":"rule-5"}
{"create":{}}
{"@timestamp":"2026-03-18T12:30:00.000Z","last_series_event_timestamp":"2026-03-18T12:20:00.000Z","actor":"user-3","action_type":"ack","group_hash":"elasticgh-10","episode_id":"ep-010","rule_id":"rule-5"}
{"create":{}}
{"@timestamp":"2026-03-18T12:40:00.000Z","last_series_event_timestamp":"2026-03-18T12:20:00.000Z","actor":"user-3","action_type":"unack","group_hash":"elasticgh-10","episode_id":"ep-010","rule_id":"rule-5"}
{"create":{}}
{"@timestamp":"2026-03-18T12:50:00.000Z","last_series_event_timestamp":"2026-03-18T12:20:00.000Z","actor":"user-3","action_type":"ack","group_hash":"elasticgh-10","episode_id":"ep-010","rule_id":"rule-5"}
```

</details>

There are up to 10 episodes with actions, all with ids like `ep-001`.

Query the new route and confirm that the results are as expected.

```
POST kbn:/internal/alerting/v2/alerts/action/_bulk_get
{
  "episode_ids": ["ep-001", "ep-002", "ep-003", "foobar"]
}
```

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
gsoldevila pushed a commit that referenced this pull request Apr 4, 2026
Closes elastic#258318
Closes elastic#258319

## Summary

Adds logic to the alert episodes table to display `.alert_actions`
information.

This includes:
- New action-specific API paths.
- Snooze
  - **Per group hash.**
- Button in the actions column opens a popover where an `until` can be
picked.
  - **When snoozed**
    - A bell shows up in the status column.
- Mouse over the bell icon to see until when the snooze is in effect.
- Unsnooze
  - **Per group hash.**
  - Clicking the button removes the snooze.
- Ack/Unack
  - **Per episode.**
  - Button in the actions column
  - When "acked", an icon shows in the status column.
- Tags
- This PR only handles displaying tags. They need to be created via API.
- Resolve/Unresolve
  - **Per group hash.**
  - Button inside the ellipsis always
- The status is turned to `inactive` **regardless of the "real"
status.**

<img width="1704" height="672" alt="Screenshot 2026-03-25 at 16 04 12"
src="https://github.com/user-attachments/assets/5ef4111a-6e0c-4114-a60e-ce5f81a86ac6"
/>


## Testing


<details> <summary>POST mock episodes</summary>

```
POST _bulk
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:00:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-1", "episode": { "id": "ep-001", "status": "pending" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:01:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-1", "episode": { "id": "ep-001", "status": "pending" }, "status": "no_data" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:02:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-1", "episode": { "id": "ep-001", "status": "inactive" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:03:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-1", "episode": { "id": "ep-001", "status": "inactive" }, "status": "no_data" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:04:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-1", "episode": { "id": "ep-001", "status": "inactive" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:05:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-1", "episode": { "id": "ep-001", "status": "pending" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:06:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-1", "episode": { "id": "ep-001", "status": "active" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:07:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-2", "episode": { "id": "ep-002", "status": "active" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:08:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-2", "episode": { "id": "ep-002", "status": "active" }, "status": "no_data" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:09:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-2", "episode": { "id": "ep-002", "status": "recovering" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:10:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-2", "episode": { "id": "ep-002", "status": "recovering" }, "status": "no_data" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:11:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-2", "episode": { "id": "ep-002", "status": "active" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:12:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-2", "episode": { "id": "ep-002", "status": "recovering" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:13:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-2", "episode": { "id": "ep-002", "status": "inactive" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:14:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-1", "episode": { "id": "ep-003", "status": "pending" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:15:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-1", "episode": { "id": "ep-003", "status": "inactive" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:16:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-4", "episode": { "id": "ep-004", "status": "pending" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:17:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-4", "episode": { "id": "ep-004", "status": "active" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:18:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-4", "episode": { "id": "ep-004", "status": "recovering" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:19:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-4", "episode": { "id": "ep-004", "status": "inactive" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:20:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-5", "episode": { "id": "ep-005", "status": "pending" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:21:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-5", "episode": { "id": "ep-005", "status": "pending" }, "status": "no_data" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:22:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "gh-5", "episode": { "id": "ep-005", "status": "inactive" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:23:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "elasticgh-9", "episode": { "id": "ep-006", "status": "pending" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:24:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "elasticgh-9", "episode": { "id": "ep-006", "status": "active" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:25:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "elasticgh-9", "episode": { "id": "ep-006", "status": "active" }, "status": "no_data" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:26:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-1" }, "group_hash": "elasticgh-9", "episode": { "id": "ep-006", "status": "inactive" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:14:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-2" }, "group_hash": "elasticgh-7", "episode": { "id": "ep-007", "status": "pending" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:15:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-2" }, "group_hash": "elasticgh-7", "episode": { "id": "ep-007", "status": "inactive" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:16:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-3" }, "group_hash": "elasticgh-8", "episode": { "id": "ep-008", "status": "pending" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:17:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-3" }, "group_hash": "elasticgh-8", "episode": { "id": "ep-008", "status": "active" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:18:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-3" }, "group_hash": "elasticgh-8", "episode": { "id": "ep-008", "status": "recovering" }, "status": "recovered" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:20:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-4" }, "group_hash": "elasticgh-9", "episode": { "id": "ep-009", "status": "pending" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:21:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-4" }, "group_hash": "elasticgh-9", "episode": { "id": "ep-009", "status": "pending" }, "status": "no_data" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:23:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-5" }, "group_hash": "elasticgh-10", "episode": { "id": "ep-010", "status": "pending" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:24:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-5" }, "group_hash": "elasticgh-10", "episode": { "id": "ep-010", "status": "active" }, "status": "breached" }
{ "create": { "_index": ".rule-events" }}
{ "@timestamp": "2026-01-27T16:25:00.000Z", "source": "internal", "type": "alert", "rule": { "id": "rule-5" }, "group_hash": "elasticgh-10", "episode": { "id": "ep-010", "status": "active" }, "status": "no_data" }
```

</details>

- In the POST above, episodes 1 and 3, and episodes 6 and 9 have the
same group hashes.
- Go to `https://localhost:5601/app/observability/alerts-v2` and try all
buttons.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
gsoldevila pushed a commit that referenced this pull request Apr 9, 2026
## Summary

Part of: elastic/security-team#15982.
(Resolves requirement `#4`)

This change introduces a dedicated **`StepCategory.KibanaCases`**
(`kibana.cases`) so Cases workflow steps are grouped under **Kibana →
Cases** in the workflow actions menu instead of sitting in the flat
Kibana list.

**Actions menu (`workflows_management`)**

- Builds a **Cases** subgroup (`id: kibana.cases`) under the Kibana
group via **`nestedGroups`**, then merges any non-empty nested group
into the parent’s **`options`** so the UI stays a normal tree of groups.
- Assigns **`pathIds`** on every group (full path from the root) so
choosing a nested group from **search** opens the correct depth (Kibana
→ Cases → …) instead of only appending the last segment.
- **`ActionsMenu`** uses `selectedOption.pathIds ?? [...currentPath,
id]` when entering a group.

**Shared spec**

- Adds **`StepCategory.KibanaCases`** in `@kbn/kbn-workflows` so step
definitions and UI routing can target the Cases bucket explicitly.

**Cases plugin**

- Updates all Cases **common workflow step** definitions to use
**`StepCategory.KibanaCases`** instead of **`StepCategory.Kibana`**.

**Agent builder**

- **`get_step_definitions_tool`**: maps connector types **`cases.*`** →
**`KibanaCases`** and keeps **`kibana.*`** → **`Kibana`**.

**Tests**

- Extends **`get_action_options.test.ts`** for nested Cases, empty Cases
group hidden, **`pathIds`**, and ordering expectations.

---

## Demo


https://github.com/user-attachments/assets/dc14c35d-f63c-4165-9c23-1590a22edf80

---
gsoldevila pushed a commit that referenced this pull request Apr 21, 2026
## Summary

Fixes this test that has been failing CI on main and 9.4 backports:
```
Fleet Cypress Tests #4 / Assets - Real API for integration with ML and transforms should install integration with ML module & transforms
```

Cause was due to new version of lmd published
([lmd-3.0.0](elastic/integrations#17626)) that
caused index template and transform names to be changed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant