Skip to content

Commit 16cfebf

Browse files
authored
[One Workflow] New Cases action menu group under Kibana (elastic#261964)
## 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 ---
1 parent b84dbac commit 16cfebf

40 files changed

Lines changed: 209 additions & 38 deletions

src/platform/packages/shared/kbn-workflows/spec/step_definition_types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export enum StepCategory {
1515
External = 'external',
1616
Ai = 'ai',
1717
Kibana = 'kibana',
18+
KibanaCases = 'kibana.cases',
1819
Data = 'data',
1920
FlowControl = 'flowControl',
2021
}

src/platform/plugins/shared/workflows_management/public/features/actions_menu_popover/lib/get_action_options.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,126 @@ describe('getActionOptions', () => {
166166
}
167167
});
168168

169+
it('should not show Cases nested group when no cases steps are registered', () => {
170+
const result = getActionOptions(mockEuiTheme, mockWorkflowsExtensions);
171+
const kibanaGroup = result.find((group) => group.id === 'kibana');
172+
173+
expect(kibanaGroup).toBeDefined();
174+
if (kibanaGroup && isActionGroup(kibanaGroup)) {
175+
expect(kibanaGroup.options).toHaveLength(0);
176+
expect(kibanaGroup.options.find((opt) => opt.id === 'kibana.cases')).toBeUndefined();
177+
}
178+
});
179+
180+
it('should place KibanaCases steps in nested Cases group under Kibana', () => {
181+
const mockConnector = {
182+
type: 'cases.createCase',
183+
description: 'Create case',
184+
};
185+
186+
const mockStepDefinition = {
187+
id: 'cases.createCase',
188+
label: 'Create case',
189+
description: 'Create a case',
190+
icon: 'casesApp',
191+
category: StepCategory.KibanaCases,
192+
inputSchema: z.object({}),
193+
outputSchema: z.object({}),
194+
};
195+
196+
(getAllConnectors as jest.Mock).mockReturnValue([mockConnector]);
197+
mockWorkflowsExtensions.getStepDefinition.mockReturnValue(mockStepDefinition as any);
198+
199+
const result = getActionOptions(mockEuiTheme, mockWorkflowsExtensions);
200+
const kibanaGroup = result.find((group) => group.id === 'kibana');
201+
202+
expect(kibanaGroup).toBeDefined();
203+
if (kibanaGroup && isActionGroup(kibanaGroup)) {
204+
expect(kibanaGroup.options).toHaveLength(1);
205+
const casesNested = kibanaGroup.options[0];
206+
expect(casesNested.id).toBe('kibana.cases');
207+
if (isActionGroup(casesNested)) {
208+
expect(casesNested.options).toHaveLength(1);
209+
expect(casesNested.options[0].id).toBe('cases.createCase');
210+
}
211+
}
212+
});
213+
214+
it('should set pathIds on groups for navigation from search', () => {
215+
const mockConnector = {
216+
type: 'cases.createCase',
217+
description: 'Create case',
218+
};
219+
220+
const mockStepDefinition = {
221+
id: 'cases.createCase',
222+
label: 'Create case',
223+
description: 'Create a case',
224+
icon: 'casesApp',
225+
category: StepCategory.KibanaCases,
226+
inputSchema: z.object({}),
227+
outputSchema: z.object({}),
228+
};
229+
230+
(getAllConnectors as jest.Mock).mockReturnValue([mockConnector]);
231+
mockWorkflowsExtensions.getStepDefinition.mockReturnValue(mockStepDefinition as any);
232+
233+
const result = getActionOptions(mockEuiTheme, mockWorkflowsExtensions);
234+
const kibanaGroup = result.find((group) => group.id === 'kibana');
235+
236+
expect(kibanaGroup).toBeDefined();
237+
if (kibanaGroup && isActionGroup(kibanaGroup)) {
238+
expect(kibanaGroup.pathIds).toEqual(['kibana']);
239+
const casesNested = kibanaGroup.options.find((opt) => opt.id === 'kibana.cases');
240+
expect(casesNested).toBeDefined();
241+
if (casesNested && isActionGroup(casesNested)) {
242+
expect(casesNested.pathIds).toEqual(['kibana', 'kibana.cases']);
243+
}
244+
}
245+
});
246+
247+
it('should list nested Cases group before other Kibana options when both are present', () => {
248+
const mockConnectors = [
249+
{
250+
type: 'kibana.saved_object',
251+
description: 'Saved Object',
252+
summary: 'Kibana Summary',
253+
},
254+
{
255+
type: 'cases.createCase',
256+
description: 'Create case',
257+
},
258+
];
259+
260+
const mockCasesStepDefinition = {
261+
id: 'cases.createCase',
262+
label: 'Create case',
263+
description: 'Create a case',
264+
icon: 'casesApp',
265+
category: StepCategory.KibanaCases,
266+
inputSchema: z.object({}),
267+
outputSchema: z.object({}),
268+
};
269+
270+
(getAllConnectors as jest.Mock).mockReturnValue(mockConnectors);
271+
mockWorkflowsExtensions.getStepDefinition.mockImplementation((type: string) => {
272+
if (type === 'cases.createCase') {
273+
return mockCasesStepDefinition as any;
274+
}
275+
return undefined;
276+
});
277+
278+
const result = getActionOptions(mockEuiTheme, mockWorkflowsExtensions);
279+
const kibanaGroup = result.find((group) => group.id === 'kibana');
280+
281+
expect(kibanaGroup).toBeDefined();
282+
if (kibanaGroup && isActionGroup(kibanaGroup)) {
283+
expect(kibanaGroup.options).toHaveLength(2);
284+
expect(kibanaGroup.options[0].id).toBe('kibana.cases');
285+
expect(kibanaGroup.options[1].id).toBe('kibana.saved_object');
286+
}
287+
});
288+
169289
it('should add elasticsearch connectors to elasticsearch group', () => {
170290
const mockConnector = {
171291
type: 'elasticsearch.search',

src/platform/plugins/shared/workflows_management/public/features/actions_menu_popover/lib/get_action_options.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,19 @@ export function getActionOptions(
8181
options: [...builtInTriggerOptions, ...registeredTriggerOptions],
8282
};
8383

84-
const kibanaGroup: ActionOptionData = {
84+
const kibanaCasesGroup: ActionGroup = {
85+
iconType: 'briefcase',
86+
id: 'kibana.cases',
87+
label: i18n.translate('workflows.actionsMenu.kibanaCases', {
88+
defaultMessage: 'Cases',
89+
}),
90+
description: i18n.translate('workflows.actionsMenu.kibanaCasesDescription', {
91+
defaultMessage: 'Create and manage cases from your workflow',
92+
}),
93+
options: [],
94+
};
95+
96+
const kibanaGroup: ActionGroup = {
8597
iconType: 'logoKibana',
8698
id: 'kibana',
8799
label: i18n.translate('workflows.actionsMenu.kibana', {
@@ -91,6 +103,7 @@ export function getActionOptions(
91103
defaultMessage: 'Work with Kibana data and features directly from your workflow',
92104
}),
93105
options: [],
106+
nestedGroups: [kibanaCasesGroup],
94107
};
95108
const externalGroup: ActionOptionData = {
96109
iconType: 'plugs',
@@ -234,6 +247,7 @@ export function getActionOptions(
234247
[StepCategory.External]: externalGroup,
235248
[StepCategory.Ai]: aiGroup,
236249
[StepCategory.Kibana]: kibanaGroup,
250+
[StepCategory.KibanaCases]: kibanaCasesGroup,
237251
[StepCategory.Data]: dataTransformationGroup,
238252
[StepCategory.FlowControl]: flowControlGroup,
239253
};
@@ -309,7 +323,17 @@ export function getActionOptions(
309323
}
310324
}
311325

312-
return [
326+
for (const group of Object.values(stepGroups)) {
327+
if (group.nestedGroups) {
328+
for (const nestedGroup of group.nestedGroups) {
329+
if (nestedGroup.options.length > 0) {
330+
group.options.unshift(nestedGroup);
331+
}
332+
}
333+
}
334+
}
335+
336+
const topLevelOptions: ActionOptionData[] = [
313337
triggersGroup,
314338
elasticSearchGroup,
315339
kibanaGroup,
@@ -318,6 +342,24 @@ export function getActionOptions(
318342
externalGroup,
319343
flowControlGroup,
320344
];
345+
assignActionPathIds(topLevelOptions);
346+
return topLevelOptions;
347+
}
348+
349+
/**
350+
* Sets `pathIds` on every nested group so navigation works when a group is chosen from search
351+
* (full ancestor chain, not only the clicked row's id).
352+
*/
353+
function assignActionPathIds(
354+
options: ActionOptionData[],
355+
parentPath: readonly string[] = []
356+
): void {
357+
for (const opt of options) {
358+
if ('options' in opt) {
359+
opt.pathIds = [...parentPath, opt.id];
360+
assignActionPathIds(opt.options, opt.pathIds);
361+
}
362+
}
321363
}
322364

323365
export function flattenOptions(options: ActionOptionData[]): ActionOptionData[] {

src/platform/plugins/shared/workflows_management/public/features/actions_menu_popover/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@ interface ActionBase {
1717
instancesLabel?: string;
1818
iconColor?: string;
1919
stability?: StepStabilityLevel;
20+
/**
21+
* Ids from the root menu down through this row (for groups: path to open this group).
22+
* Set in `getActionOptions` for O(1) navigation when selecting from search.
23+
*/
24+
pathIds?: readonly string[];
2025
}
2126

2227
export interface ActionGroup extends ActionBase {
2328
iconType: IconType;
2429
options: ActionOptionData[];
30+
nestedGroups?: ActionGroup[];
2531
}
2632

2733
export interface ActionConnectorGroup extends ActionBase {

src/platform/plugins/shared/workflows_management/public/features/actions_menu_popover/ui/actions_menu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ export function ActionsMenu({ onActionSelected }: ActionsMenuProps) {
146146
// eslint-disable-next-line @typescript-eslint/no-explicit-any
147147
const handleChange = (_: Array<ActionOptionData>, __: any, selectedOption: ActionOptionData) => {
148148
if (isActionGroup(selectedOption)) {
149-
setCurrentPath([...currentPath, selectedOption.id]);
149+
const nextPath = selectedOption.pathIds ?? [...currentPath, selectedOption.id];
150+
setCurrentPath([...nextPath]);
150151
setSearchTerm('');
151152
setOptions(selectedOption.options);
152153
} else {

src/platform/plugins/shared/workflows_management/server/agent_builder/tools/get_step_definitions_tool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ export async function resolveConnectors(
6161
}
6262

6363
function categorizeConnectorType(type: string): StepCategory {
64-
if (type.startsWith('kibana.') || type.startsWith('cases.')) return StepCategory.Kibana;
64+
if (type.startsWith('kibana.')) return StepCategory.Kibana;
65+
if (type.startsWith('cases.')) return StepCategory.KibanaCases;
6566
if (type.startsWith('elasticsearch.')) return StepCategory.Elasticsearch;
6667
if (type.startsWith('ai.')) return StepCategory.Ai;
6768
if (type.startsWith('data.')) return StepCategory.Data;

x-pack/platform/plugins/shared/cases/common/workflows/steps/add_alerts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const addAlertsStepCommonDefinition: CommonStepDefinition<
4141
AddAlertsStepOutputSchema
4242
> = {
4343
id: AddAlertsStepTypeId,
44-
category: StepCategory.Kibana,
44+
category: StepCategory.KibanaCases,
4545
label: i18n.ADD_ALERTS_STEP_LABEL,
4646
description: i18n.ADD_ALERTS_STEP_DESCRIPTION,
4747
documentation: {

x-pack/platform/plugins/shared/cases/common/workflows/steps/add_comment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const addCommentStepCommonDefinition: CommonStepDefinition<
3434
AddCommentStepOutputSchema
3535
> = {
3636
id: AddCommentStepTypeId,
37-
category: StepCategory.Kibana,
37+
category: StepCategory.KibanaCases,
3838
label: i18n.ADD_COMMENT_STEP_LABEL,
3939
description: i18n.ADD_COMMENT_STEP_DESCRIPTION,
4040
documentation: {

x-pack/platform/plugins/shared/cases/common/workflows/steps/add_events.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const addEventsStepCommonDefinition: CommonStepDefinition<
3939
AddEventsStepOutputSchema
4040
> = {
4141
id: AddEventsStepTypeId,
42-
category: StepCategory.Kibana,
42+
category: StepCategory.KibanaCases,
4343
label: i18n.ADD_EVENTS_STEP_LABEL,
4444
description: i18n.ADD_EVENTS_STEP_DESCRIPTION,
4545
documentation: {

x-pack/platform/plugins/shared/cases/common/workflows/steps/add_observables.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const addObservablesStepCommonDefinition: CommonStepDefinition<
4444
AddObservablesStepOutputSchema
4545
> = {
4646
id: AddObservablesStepTypeId,
47-
category: StepCategory.Kibana,
47+
category: StepCategory.KibanaCases,
4848
label: i18n.ADD_OBSERVABLES_STEP_LABEL,
4949
description: i18n.ADD_OBSERVABLES_STEP_DESCRIPTION,
5050
documentation: {

0 commit comments

Comments
 (0)