Skip to content

Commit 3a38cbb

Browse files
yngrdynkibanamachinecursoragent
authored
[One workflow] Event-driven triggers testing UX (#268832)
Relates to elastic/security-team#16717 ## Summary This change improves how users **test and inspect event-driven workflows** from the run workflow flow. The legacy “event” JSON editor path is split from **alert** configuration, the execute modal gains clearer **trigger tab** behavior (including defaulting to the **Event** tab when the definition uses custom event triggers), and the **Event** tab surfaces **historical rows from `.workflows-events`** with KQL, time range, and a document grid so users can pick a prior payload to reuse. ### User-visible changes - **Run workflow modal**: Trigger tabs refined; **Alert** configuration lives in **`WorkflowExecuteAlertForm`**; **Event** tab uses **`WorkflowExecuteEventForm`** with search + table + payload selection workflow. - **Event tab**: Search bar (KQL), date range, paginated results, column picker aligned to `.workflows-events` fields. ### Modal trigger selection ```mermaid flowchart LR subgraph Modal[Workflow execute modal] T[Trigger tabs] A[Alert form] M[Manual / Index / …] E[Event form] end T --> A T --> M T --> E E --> Q[KQL + time range] E --> G[UnifiedDataTable on log hits] G --> S[Select row → seed JSON editor] ``` ## 🎥 Demo https://github.com/user-attachments/assets/65a4643c-a255-4dde-a29d-ee66567e0a05 ## Follow ups - Filter Run Tabs according to triggers in the workflow - Clean up the toolbar, and remove things like `sort fields` - Unify table for Alerts and Document --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2885edf commit 3a38cbb

61 files changed

Lines changed: 5612 additions & 696 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/platform/packages/shared/kbn-workflows-ui/src/api/types.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
WorkflowDetailDto,
1515
WorkflowExecutionSortField,
1616
WorkflowExecutionSortOrder,
17+
WorkflowsEventsLogDocumentSource,
1718
} from '@kbn/workflows';
1819

1920
export interface BulkCreateWorkflowsParams {
@@ -140,3 +141,23 @@ export interface ResumeExecutionParams {
140141
export interface WorkflowsConfig {
141142
eventDrivenExecutionEnabled: boolean;
142143
}
144+
145+
export interface SearchTriggerEventLogParams {
146+
kql?: string;
147+
from?: string;
148+
to?: string;
149+
page?: number;
150+
size?: number;
151+
}
152+
153+
export interface SearchTriggerEventLogHit {
154+
id: string;
155+
source: WorkflowsEventsLogDocumentSource;
156+
}
157+
158+
export interface SearchTriggerEventLogResult {
159+
hits: SearchTriggerEventLogHit[];
160+
total: number;
161+
page: number;
162+
size: number;
163+
}

src/platform/packages/shared/kbn-workflows-ui/src/api/workflows_api.mock.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@ export const createMockWorkflowApi = (): MockWorkflowApi =>
4545
getChildrenExecutions: jest.fn(),
4646

4747
getConfig: jest.fn(),
48+
searchTriggerEvents: jest.fn(),
4849
} as unknown as MockWorkflowApi);

src/platform/packages/shared/kbn-workflows-ui/src/api/workflows_api.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,24 @@ describe('WorkflowApi', () => {
237237
});
238238
});
239239

240+
describe('searchTriggerEvents', () => {
241+
it('should call POST /internal/workflows/trigger_events/_search with body', async () => {
242+
const params = {
243+
kql: 'eventId: "e1"',
244+
from: '2025-01-01',
245+
to: '2025-12-31',
246+
page: 2,
247+
size: 25,
248+
};
249+
await api.searchTriggerEvents(params);
250+
251+
expect(http.post).toHaveBeenCalledWith('/internal/workflows/trigger_events/_search', {
252+
body: JSON.stringify(params),
253+
version: INTERNAL_VERSION,
254+
});
255+
});
256+
});
257+
240258
// ---------------------------------------------------------------------------
241259
// Execution operations
242260
// ---------------------------------------------------------------------------

src/platform/packages/shared/kbn-workflows-ui/src/api/workflows_api.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import type {
4444
MgetWorkflowsParams,
4545
ResumeExecutionParams,
4646
RunWorkflowOptions,
47+
SearchTriggerEventLogParams,
48+
SearchTriggerEventLogResult,
4749
TestWorkflowParams,
4850
UpdateWorkflowParams,
4951
ValidateWorkflowParams,
@@ -292,4 +294,13 @@ export class WorkflowApi {
292294
version: INTERNAL_API_VERSION,
293295
});
294296
}
297+
298+
async searchTriggerEvents(
299+
params: SearchTriggerEventLogParams
300+
): Promise<SearchTriggerEventLogResult> {
301+
return this.http.post(`${INTERNAL_BASE}/trigger_events/_search`, {
302+
body: JSON.stringify(params),
303+
version: INTERNAL_API_VERSION,
304+
});
305+
}
295306
}

src/platform/packages/shared/kbn-workflows-ui/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from './use_workflows';
11+
export * from './use_query_trigger_events';
1112
export * from './use_run_workflow';
1213
export * from './use_workflows_capabilities';
1314
export * from './use_workflows_ui_settings';
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { renderHook, waitFor } from '@testing-library/react';
11+
import React from 'react';
12+
import { QueryClient, QueryClientProvider } from '@kbn/react-query';
13+
import {
14+
getWorkflowTriggerEventsLogQueryKey,
15+
useQueryTriggerEvents,
16+
} from './use_query_trigger_events';
17+
import type { SearchTriggerEventLogResult } from '../api/types';
18+
import { createMockWorkflowApi } from '../api/workflows_api.mock';
19+
import { testQueryClientConfig } from '../test_utils';
20+
21+
jest.mock('@kbn/kibana-react-plugin/public', () => ({
22+
useKibana: jest.fn(),
23+
}));
24+
25+
const mockWorkflowApi = createMockWorkflowApi();
26+
jest.mock('../api/use_workflows_api', () => ({
27+
useWorkflowsApi: () => mockWorkflowApi,
28+
}));
29+
30+
const queryClient = new QueryClient(testQueryClientConfig);
31+
32+
const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) =>
33+
React.createElement(QueryClientProvider, { client: queryClient }, children);
34+
35+
describe('getWorkflowTriggerEventsLogQueryKey', () => {
36+
it('returns the same key for equivalent param objects', () => {
37+
const paramsA = {
38+
page: 1,
39+
size: 10,
40+
from: '2025-01-01',
41+
to: '2025-01-02',
42+
kql: 'triggerId: foo',
43+
};
44+
const paramsB = {
45+
kql: 'triggerId: foo',
46+
from: '2025-01-01',
47+
to: '2025-01-02',
48+
page: 1,
49+
size: 10,
50+
};
51+
52+
expect(getWorkflowTriggerEventsLogQueryKey(paramsA)).toEqual(
53+
getWorkflowTriggerEventsLogQueryKey(paramsB)
54+
);
55+
});
56+
57+
it('omits optional fields as undefined in the key tuple', () => {
58+
expect(getWorkflowTriggerEventsLogQueryKey({ page: 2, size: 50 })).toEqual([
59+
'workflowTriggerEventsLog',
60+
undefined,
61+
undefined,
62+
undefined,
63+
2,
64+
50,
65+
]);
66+
});
67+
});
68+
69+
describe('useQueryTriggerEvents', () => {
70+
beforeEach(() => {
71+
jest.clearAllMocks();
72+
queryClient.clear();
73+
});
74+
75+
it('calls searchTriggerEvents with correct params', async () => {
76+
const mockData: SearchTriggerEventLogResult = {
77+
hits: [],
78+
total: 0,
79+
page: 1,
80+
size: 10,
81+
};
82+
const params = { page: 1, size: 10, from: '2025-01-01' };
83+
mockWorkflowApi.searchTriggerEvents.mockResolvedValue(mockData);
84+
85+
const { result } = renderHook(() => useQueryTriggerEvents(params), { wrapper });
86+
87+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
88+
89+
expect(mockWorkflowApi.searchTriggerEvents).toHaveBeenCalledWith(params);
90+
expect(result.current.data).toEqual(mockData);
91+
});
92+
93+
it('does not refetch when params object identity changes but values are unchanged', async () => {
94+
const mockData: SearchTriggerEventLogResult = {
95+
hits: [],
96+
total: 0,
97+
page: 1,
98+
size: 10,
99+
};
100+
mockWorkflowApi.searchTriggerEvents.mockResolvedValue(mockData);
101+
102+
const initialParams = { page: 1, size: 10, from: '2025-01-01', to: '2025-01-02' };
103+
const { result, rerender } = renderHook(
104+
({ params }: { params: typeof initialParams }) => useQueryTriggerEvents(params),
105+
{ wrapper, initialProps: { params: initialParams } }
106+
);
107+
108+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
109+
expect(mockWorkflowApi.searchTriggerEvents).toHaveBeenCalledTimes(1);
110+
111+
rerender({ params: { ...initialParams } });
112+
113+
await waitFor(() => expect(result.current.isFetching).toBe(false));
114+
expect(mockWorkflowApi.searchTriggerEvents).toHaveBeenCalledTimes(1);
115+
});
116+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { useQuery } from '@kbn/react-query';
11+
import type { SearchTriggerEventLogParams, SearchTriggerEventLogResult } from '../api/types';
12+
import { useWorkflowsApi } from '../api/use_workflows_api';
13+
14+
export type WorkflowTriggerEventsLogQueryKey = readonly [
15+
'workflowTriggerEventsLog',
16+
string | undefined,
17+
string | undefined,
18+
string | undefined,
19+
number | undefined,
20+
number | undefined
21+
];
22+
23+
/**
24+
* Primitive query-key parts so equivalent searches share a cache entry even when
25+
* callers pass a new `params` object reference each render.
26+
*/
27+
export const getWorkflowTriggerEventsLogQueryKey = (
28+
params: SearchTriggerEventLogParams
29+
): WorkflowTriggerEventsLogQueryKey => [
30+
'workflowTriggerEventsLog',
31+
params.kql,
32+
params.from,
33+
params.to,
34+
params.page,
35+
params.size,
36+
];
37+
38+
export function useQueryTriggerEvents(
39+
params: SearchTriggerEventLogParams,
40+
options?: { enabled?: boolean }
41+
) {
42+
const api = useWorkflowsApi();
43+
44+
return useQuery<SearchTriggerEventLogResult>({
45+
networkMode: 'always',
46+
queryKey: getWorkflowTriggerEventsLogQueryKey(params),
47+
queryFn: () => api.searchTriggerEvents(params),
48+
enabled: options?.enabled ?? true,
49+
keepPreviousData: true,
50+
});
51+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
/**
11+
* Shape of a `.workflows-events` document returned by trigger-event log search.
12+
* Mirrors the execution engine trigger-event document shape.
13+
*/
14+
export interface WorkflowsEventsLogDocumentSource {
15+
'@timestamp': string;
16+
eventId: string;
17+
triggerId: string;
18+
spaceId: string;
19+
subscriptions: string[];
20+
sourceExecutionId?: string;
21+
payload: Record<string, unknown>;
22+
}
23+
24+
/**
25+
* `_source` from a trigger-event log hit used to build a manual replay payload.
26+
* Fields other than `payload` are ignored today but documented for callers.
27+
*/
28+
export type TriggerEventReplaySource = Partial<WorkflowsEventsLogDocumentSource>;
29+
30+
/**
31+
* Platform fields injected when replaying a logged trigger event from the Event tab.
32+
* Custom trigger payload properties are merged at the same level (see {@link EventTriggerReplayEvent}).
33+
*/
34+
export interface EventTriggerReplayPlatformFields {
35+
/** ISO-8601 time for this test run (not the logged `@timestamp`). */
36+
timestamp: string;
37+
/** Active Kibana space for the replay run. */
38+
spaceId: string;
39+
/** Reset for manual replay so chain depth does not carry over from production dispatch. */
40+
eventChainDepth: 0;
41+
/** Reset for manual replay so cycle detection starts fresh. */
42+
eventChainVisitedWorkflowIds: string[];
43+
}
44+
45+
/**
46+
* `event` object passed to a manual / test workflow run for an event-driven trigger.
47+
* Trigger-specific payload fields from the log entry are spread alongside platform fields.
48+
*/
49+
export type EventTriggerReplayEvent = EventTriggerReplayPlatformFields & Record<string, unknown>;
50+
51+
/**
52+
* Top-level workflow run `inputs` JSON built when selecting a row in the Event trigger picker.
53+
*/
54+
export interface EventTriggerReplayInput {
55+
event: EventTriggerReplayEvent;
56+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { DataViewBase, DataViewFieldBase } from '@kbn/es-query';
11+
12+
export const WORKFLOWS_EVENTS_DATA_STREAM = '.workflows-events';
13+
14+
/**
15+
* Upper bound passed to Elasticsearch `track_total_hits` when searching `.workflows-events`.
16+
*
17+
* Totals at or above this value are a lower bound only — the index may contain more matching
18+
* documents. The trigger-event search API returns `total: WORKFLOWS_EVENTS_TRACK_TOTAL_HITS_CAP`
19+
* in that case; clients should display `10,000+` (or equivalent) rather than an exact count.
20+
*
21+
* Matches UnifiedDataTable's `MAX_LOADED_GRID_ROWS` (10_000) so the Event tab grid and reported
22+
* total stay aligned.
23+
*/
24+
export const WORKFLOWS_EVENTS_TRACK_TOTAL_HITS_CAP = 10_000;
25+
26+
/**
27+
* KQL field metadata for `.workflows-events`, shared by server and client:
28+
*
29+
* - **Server** ({@link WORKFLOWS_EVENTS_DATA_VIEW}): passed to `toElasticsearchQuery` in the
30+
* execution engine. That path has no access to registered browser data views, so we use a static
31+
* `DataViewBase` built from this list.
32+
* - **Client** (Event trigger picker): creates an ephemeral ad-hoc data view for the KQL bar, then
33+
* replaces its fields with this same list so autocomplete and server translation stay aligned.
34+
*
35+
* `spaceId` is intentionally omitted — the search API always scopes results to the request space.
36+
*/
37+
export const WORKFLOWS_EVENTS_DATA_VIEW_FIELDS: DataViewFieldBase[] = [
38+
{ name: '@timestamp', type: 'date', esTypes: ['date'] },
39+
{ name: 'eventId', type: 'string', esTypes: ['keyword'] },
40+
{ name: 'triggerId', type: 'string', esTypes: ['keyword'] },
41+
{ name: 'sourceExecutionId', type: 'string', esTypes: ['keyword'] },
42+
{ name: 'subscriptions', type: 'string', esTypes: ['keyword'] },
43+
{ name: 'payload', type: 'object', esTypes: ['object'] },
44+
];
45+
46+
/** Static data view used for server-side KQL → Elasticsearch translation. */
47+
export const WORKFLOWS_EVENTS_DATA_VIEW: DataViewBase = {
48+
title: WORKFLOWS_EVENTS_DATA_STREAM,
49+
fields: [...WORKFLOWS_EVENTS_DATA_VIEW_FIELDS],
50+
};

src/platform/packages/shared/kbn-workflows/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export * from './spec/deprecated_step_metadata';
3939
export * from './types/latest';
4040
export * from './types/utils';
4141
export * from './common/constants';
42+
export * from './common/workflows_events';
43+
export type * from './common/event_trigger_replay';
4244
export * from './common/well_known_trigger_sources';
4345
export type { WorkflowExecutionEventDispatchMetadata } from './common/workflow_execution_schedule_metadata';
4446
export * from './common/privileges';

0 commit comments

Comments
 (0)