Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5f18cbf
move domain errors to errors folder
skynetigor Oct 30, 2025
e25b5db
Add ability to call testWorkflow in the context of created workflow
skynetigor Oct 30, 2025
e2051de
add workflowId for WorkflowDetailTestModal to link test execution to …
skynetigor Oct 30, 2025
45d0fef
add executionType filter support
skynetigor Oct 31, 2025
e6b8f6e
add isTestRun to mapping
skynetigor Oct 31, 2025
c2d7700
Call createOrUpdateIndex to be able to update the index of workflow e…
skynetigor Oct 31, 2025
ae8df6d
fix unit-tests
skynetigor Oct 31, 2025
0edd957
Create workflows_management_api.test.ts
skynetigor Oct 31, 2025
8fee7ff
add runWorkflowThunk
skynetigor Oct 31, 2025
48baabd
Update test_workflow_thunk.ts
skynetigor Oct 31, 2025
87f4d6e
Change WorkflowDetailTestModal and write tests
skynetigor Oct 31, 2025
4718893
Update test_workflow_thunk.test.ts
skynetigor Oct 31, 2025
52011fb
Update types.ts
skynetigor Oct 31, 2025
b537fb6
Merge branch 'main' into 14414-Persist-all-workflow-execution-&-remov…
skynetigor Oct 31, 2025
725b326
Changes from node scripts/lint_ts_projects --fix
kibanamachine Oct 31, 2025
af4afeb
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Oct 31, 2025
548582f
Merge branch '14414-Persist-all-workflow-execution-&-remove-test-run-…
skynetigor Nov 3, 2025
633cf34
Update workflows_management_service.ts
skynetigor Nov 3, 2025
5beb84c
add test indicator for workflow executions
skynetigor Nov 3, 2025
d2eecb7
Merge branch 'main' into 14414-Persist-all-workflow-execution-&-remov…
skynetigor Nov 3, 2025
dab2b8c
fix type issues
skynetigor Nov 3, 2025
2987fe1
add tooltip for test execution badge
skynetigor Nov 3, 2025
94c4f20
Merge branch '14414-Persist-all-workflow-execution-&-remove-test-run-…
skynetigor Nov 3, 2025
baafe41
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Nov 3, 2025
9387dac
Merge branch 'main' into 14414-Persist-all-workflow-execution-&-remov…
skynetigor Nov 3, 2025
49010c3
Merge branch '14414-Persist-all-workflow-execution-&-remove-test-run-…
skynetigor Nov 3, 2025
fab08fb
Update workflow_execution_panel.tsx
skynetigor Nov 3, 2025
8739cf6
Update workflows_management_service.test.ts
skynetigor Nov 3, 2025
ea9a061
Merge branch 'main' into 14414-Persist-all-workflow-execution-&-remov…
skynetigor Nov 7, 2025
b031f11
Update workflow_detail_test_modal.tsx
skynetigor Nov 7, 2025
da98cd6
Update workflow_detail_test_modal.tsx
skynetigor Nov 7, 2025
7b4b790
Update workflow_detail_test_modal.tsx
skynetigor Nov 7, 2025
02fbbe2
fix tests
skynetigor Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export { WorkflowExecutionNotFoundError } from './workflow_execution_not_found_error';
export { WorkflowNotFoundError } from './workflow_not_found_error';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export class WorkflowNotFoundError extends Error {
constructor(workflowId: string) {
super(`Workflow with id "${workflowId}" not found.`);
this.name = 'WorkflowNotFoundError';
}
}
1 change: 1 addition & 0 deletions src/platform/packages/shared/kbn-workflows/types/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export interface WorkflowExecutionDto {
spaceId: string;
id: string;
status: ExecutionStatus;
isTestRun: boolean;
startedAt: string;
finishedAt: string;
workflowId?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { WorkflowDetailDto } from '@kbn/workflows';
import { runWorkflowThunk } from './run_workflow_thunk';
import { createMockStore, getMockServices } from '../../__mocks__/store.mock';
import type { MockServices, MockStore } from '../../__mocks__/store.mock';

describe('runWorkflowThunk', () => {
let store: MockStore;
let mockServices: MockServices;

const mockWorkflow = {
id: 'workflow-1',
} as WorkflowDetailDto;

beforeEach(() => {
jest.clearAllMocks();

store = createMockStore();
mockServices = getMockServices(store);
});

it('should run workflow successfully', async () => {
const mockResponse = {
workflowExecutionId: 'execution-123',
};

const testInputs = {
param1: 'value1',
param2: 'value2',
};

// Set up state with mock workflow
store.dispatch({ type: 'detail/setWorkflow', payload: mockWorkflow });

mockServices.http.post.mockResolvedValue(mockResponse);

const result = await store.dispatch(runWorkflowThunk({ inputs: testInputs }));

expect(mockServices.http.post).toHaveBeenCalledWith(`/api/workflows/${mockWorkflow.id}/run`, {
body: JSON.stringify({
inputs: testInputs,
}),
});
expect(mockServices.notifications.toasts.addSuccess).toHaveBeenCalledWith(
'Workflow execution started',
{ toastLifeTimeMs: 2000 }
);
expect(result.type).toBe('detail/runWorkflowThunk/fulfilled');
expect(result.payload).toEqual(mockResponse);
});

it('should reject when no workflow in state', async () => {
// Set up state with no workflow
store.dispatch({ type: 'detail/setWorkflow', payload: null });

const result = await store.dispatch(runWorkflowThunk({ inputs: {} }));

expect(result.type).toBe('detail/runWorkflowThunk/rejected');
expect(result.payload).toBe('No workflow to run');
});

it('should handle HTTP error with body message', async () => {
const error = {
body: { message: 'Failed to run workflow' },
message: 'Bad Request',
};

store.dispatch({ type: 'detail/setWorkflow', payload: mockWorkflow });
mockServices.http.post.mockRejectedValue(error);

const result = await store.dispatch(runWorkflowThunk({ inputs: {} }));

expect(mockServices.notifications.toasts.addError).toHaveBeenCalledWith(
new Error('Failed to run workflow'),
{
title: 'Failed to run workflow',
}
);
expect(result.type).toBe('detail/runWorkflowThunk/rejected');
expect(result.payload).toBe('Failed to run workflow');
});

it('should handle HTTP error without body message', async () => {
const error = {
message: 'Network Error',
};

store.dispatch({ type: 'detail/setWorkflow', payload: mockWorkflow });
mockServices.http.post.mockRejectedValue(error);

const result = await store.dispatch(runWorkflowThunk({ inputs: {} }));

expect(mockServices.notifications.toasts.addError).toHaveBeenCalledWith(
new Error('Network Error'),
{
title: 'Failed to run workflow',
}
);
expect(result.type).toBe('detail/runWorkflowThunk/rejected');
expect(result.payload).toBe('Network Error');
});

it('should handle error without message', async () => {
const error = {};

store.dispatch({ type: 'detail/setWorkflow', payload: mockWorkflow });
mockServices.http.post.mockRejectedValue(error);

const result = await store.dispatch(runWorkflowThunk({ inputs: {} }));

expect(mockServices.notifications.toasts.addError).toHaveBeenCalledWith(
new Error('Failed to run workflow'),
{
title: 'Failed to run workflow',
}
);
expect(result.type).toBe('detail/runWorkflowThunk/rejected');
expect(result.payload).toBe('Failed to run workflow');
});

it('should handle empty inputs object', async () => {
const mockResponse = {
workflowExecutionId: 'execution-456',
};

store.dispatch({ type: 'detail/setWorkflow', payload: mockWorkflow });
mockServices.http.post.mockResolvedValue(mockResponse);

const result = await store.dispatch(runWorkflowThunk({ inputs: {} }));

expect(mockServices.http.post).toHaveBeenCalledWith(`/api/workflows/${mockWorkflow.id}/run`, {
body: JSON.stringify({
inputs: {},
}),
});
expect(mockServices.notifications.toasts.addSuccess).toHaveBeenCalledWith(
'Workflow execution started',
{ toastLifeTimeMs: 2000 }
);
expect(result.type).toBe('detail/runWorkflowThunk/fulfilled');
expect(result.payload).toEqual(mockResponse);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { createAsyncThunk } from '@reduxjs/toolkit';
import { i18n } from '@kbn/i18n';
import type { WorkflowsServices } from '../../../../../types';
import type { RootState } from '../../types';
import { selectWorkflow } from '../selectors';

export interface RunWorkflowParams {
inputs: Record<string, unknown>;
}

export interface RunWorkflowResponse {
workflowExecutionId: string;
}

export const runWorkflowThunk = createAsyncThunk<
RunWorkflowResponse,
RunWorkflowParams,
{ state: RootState; extra: { services: WorkflowsServices } }
>(
'detail/runWorkflowThunk',
async ({ inputs }, { getState, rejectWithValue, extra: { services } }) => {
const { http, notifications } = services;
try {
const workflow = selectWorkflow(getState());

if (!workflow) {
return rejectWithValue('No workflow to run');
}

// Make the API call to run the workflow
const response = await http.post<RunWorkflowResponse>(`/api/workflows/${workflow.id}/run`, {
body: JSON.stringify({
inputs,
}),
});
// Show success notification
notifications.toasts.addSuccess(
i18n.translate('workflows.detail.runWorkflow.success', {
defaultMessage: 'Workflow execution started',
}),
{ toastLifeTimeMs: 2000 }
);
return response;
} catch (error) {
// Extract error message from HTTP error body if available
const errorMessage = error.body?.message || error.message || 'Failed to run workflow';

notifications.toasts.addError(new Error(errorMessage), {
title: i18n.translate('workflows.detail.runWorkflow.error', {
defaultMessage: 'Failed to run workflow',
}),
});
return rejectWithValue(errorMessage);
}
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('testWorkflowThunk', () => {
mockServices = getMockServices(store);
});

it('should test workflow successfully', async () => {
it('should test workflow successfully without workflow id', async () => {
const mockResponse = {
workflowExecutionId: 'execution-123',
};
Expand All @@ -34,6 +34,8 @@ describe('testWorkflowThunk', () => {

// Set up state with yaml content
store.dispatch({ type: 'detail/setYamlString', payload: 'name: Test Workflow\nsteps: []' });
// Set workflow to undefined
store.dispatch({ type: 'detail/setWorkflow', payload: undefined });

mockServices.http.post.mockResolvedValue(mockResponse);

Expand All @@ -53,6 +55,40 @@ describe('testWorkflowThunk', () => {
expect(result.payload).toEqual(mockResponse);
});

it('should test workflow successfully with workflow id when workflow is available', async () => {
const mockResponse = {
workflowExecutionId: 'execution-123',
};

const testInputs = {
param1: 'value1',
param2: 'value2',
};

// Set up state with yaml content
store.dispatch({ type: 'detail/setYamlString', payload: 'name: Test Workflow\nsteps: []' });
// Set workflow
store.dispatch({ type: 'detail/setWorkflow', payload: { id: 'workflow-123' } });

mockServices.http.post.mockResolvedValue(mockResponse);

const result = await store.dispatch(testWorkflowThunk({ inputs: testInputs }));

expect(mockServices.http.post).toHaveBeenCalledWith('/api/workflows/test', {
body: JSON.stringify({
workflowYaml: 'name: Test Workflow\nsteps: []',
inputs: testInputs,
workflowId: 'workflow-123',
}),
});
expect(mockServices.notifications.toasts.addSuccess).toHaveBeenCalledWith(
'Workflow test execution started',
{ toastLifeTimeMs: 2000 }
);
expect(result.type).toBe('detail/testWorkflowThunk/fulfilled');
expect(result.payload).toEqual(mockResponse);
});

it('should reject when no YAML content to test', async () => {
// Set up state with empty yaml
store.dispatch({ type: 'detail/setYamlString', payload: '' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
import { i18n } from '@kbn/i18n';
import type { WorkflowsServices } from '../../../../../types';
import type { RootState } from '../../types';
import { selectYamlString } from '../selectors';
import { selectWorkflow, selectYamlString } from '../selectors';

export interface TestWorkflowParams {
inputs: Record<string, unknown>;
Expand All @@ -31,13 +31,24 @@ export const testWorkflowThunk = createAsyncThunk<
const { http, notifications } = services;
try {
const yamlString = selectYamlString(getState());
const workflow = selectWorkflow(getState());

if (!yamlString) {
return rejectWithValue('No YAML content to test');
}

const requestBody: Record<string, unknown> = {
workflowYaml: yamlString,
inputs,
};

if (workflow?.id) {
requestBody.workflowId = workflow.id;
}

// Make the API call to test the workflow
const response = await http.post<TestWorkflowResponse>(`/api/workflows/test`, {
body: JSON.stringify({ workflowYaml: yamlString, inputs }),
body: JSON.stringify(requestBody),
});

// Show success notification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('useWorkflowExecutionPolling', () => {
spaceId: 'default',
id: mockWorkflowExecutionId,
status,
isTestRun: false,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
workflowId: 'test-workflow-id',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export const Default: StoryObj<typeof WorkflowStepExecutionTree> = {
workflowDefinition: definition,
yaml,
status: ExecutionStatus.COMPLETED,
isTestRun: false,
triggeredBy: 'manual',
startedAt: '2025-09-02T20:43:57.441Z',
finishedAt: '2025-09-02T20:44:15.945Z',
Expand Down Expand Up @@ -491,6 +492,7 @@ export const NoStepExecutionsExecuting: StoryObj<typeof WorkflowStepExecutionTre
workflowDefinition: definition,
yaml,
status: ExecutionStatus.RUNNING,
isTestRun: false,
triggeredBy: 'manual',
startedAt: '2025-09-02T20:43:57.441Z',
finishedAt: '2025-09-02T20:44:15.945Z',
Expand All @@ -512,6 +514,7 @@ export const NoStepExecutions: StoryObj<typeof WorkflowStepExecutionTree> = {
workflowDefinition: definition,
yaml,
status: ExecutionStatus.COMPLETED,
isTestRun: false,
triggeredBy: 'manual',
startedAt: '2025-09-02T20:43:57.441Z',
finishedAt: '2025-09-02T20:44:15.945Z',
Expand Down
Loading