Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
77f117e
includeInput/includeOutput query params
rosomri Feb 17, 2026
9818adf
lazy-load step execution I/O and exclude from polling + Clear cached …
rosomri Feb 17, 2026
4005027
linting
rosomri Feb 17, 2026
c1981ee
working with debug logs
rosomri Feb 17, 2026
39db75b
hover to lazy load step IO
rosomri Feb 17, 2026
617b77c
Merge branch 'main' of https://github.com/elastic/kibana into break_e…
rosomri Feb 17, 2026
baf1920
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Feb 17, 2026
901acce
resolve self CR comments
rosomri Feb 18, 2026
c6c4f9d
add tests for fetcher
rosomri Feb 18, 2026
9803c44
CR second iter
rosomri Feb 18, 2026
bf8f496
remove commented console logs
rosomri Feb 18, 2026
184be5b
Merge branch 'main' of https://github.com/elastic/kibana into break_e…
rosomri Feb 18, 2026
cfbad8d
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Feb 18, 2026
a469d37
Only trust the cache for terminal steps
rosomri Feb 18, 2026
c4a871d
Merge branch 'break_execution_api' of https://github.com/rosomri/kiba…
rosomri Feb 18, 2026
37bedda
type
rosomri Feb 18, 2026
6ac28b7
Merge branch 'main' of https://github.com/elastic/kibana into break_e…
rosomri Feb 18, 2026
8f5565a
Merge branch 'main' into break_execution_api
rosomri Feb 18, 2026
419a7e7
Merge branch 'main' into break_execution_api
rosomri Feb 18, 2026
bc0bf70
Merge branch 'main' of https://github.com/elastic/kibana into break_e…
rosomri Feb 19, 2026
e0adfaa
indlude IO to false by default
rosomri Feb 19, 2026
5cbe34a
Merge branch 'main' into break_execution_api
rosomri Feb 19, 2026
c2d7472
Merge branch 'main' of https://github.com/elastic/kibana into break_e…
rosomri Feb 19, 2026
ddf4b4a
'should return workflow execution with steps, excluding I/O by default
rosomri Feb 19, 2026
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
Expand Up @@ -32,8 +32,10 @@ export const loadExecutionThunk = createAsyncThunk<
try {
const previousExecution = getState().detail.execution;

// Make the API call to load the execution
const response = await http.get<WorkflowExecutionDto>(`/api/workflowExecutions/${id}`);
// Make the API call to load the execution (without input/output to reduce payload during polling)
const response = await http.get<WorkflowExecutionDto>(`/api/workflowExecutions/${id}`, {
query: { includeInput: false, includeOutput: false },
});
dispatch(setExecution(response));

if (id !== previousExecution?.id) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* 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 { renderHook, waitFor } from '@testing-library/react';
import React from 'react';
import { QueryClient, QueryClientProvider } from '@kbn/react-query';
import { ExecutionStatus } from '@kbn/workflows';
import { useStepExecution } from './use_step_execution';
import { useKibana } from '../../../hooks/use_kibana';

jest.mock('../../../hooks/use_kibana');
const mockUseKibana = useKibana as jest.MockedFunction<typeof useKibana>;

const createWrapper = (queryClient: QueryClient) => {
const Wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
return Wrapper;
};

describe('useStepExecution', () => {
let mockHttpGet: jest.Mock;
let queryClient: QueryClient;

const stepResponse = {
stepId: 'step-1',
status: 'completed',
input: { arg: 'value' },
output: { result: 'ok' },
};

beforeEach(() => {
jest.useFakeTimers();
mockHttpGet = jest.fn().mockResolvedValue(stepResponse);
mockUseKibana.mockReturnValue({
services: { http: { get: mockHttpGet } },
} as any);
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
});

afterEach(() => {
jest.useRealTimers();
queryClient.clear();
});

it('should not fetch when stepExecutionId is undefined', () => {
const { result } = renderHook(
() => useStepExecution('exec-1', undefined, ExecutionStatus.COMPLETED),
{ wrapper: createWrapper(queryClient) }
);

expect(result.current.isFetching).toBe(false);
expect(mockHttpGet).not.toHaveBeenCalled();
});

it('should fetch when both IDs are provided', async () => {
const { result } = renderHook(
() => useStepExecution('exec-1', 'step-doc-1', ExecutionStatus.COMPLETED),
{ wrapper: createWrapper(queryClient) }
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(mockHttpGet).toHaveBeenCalledWith('/api/workflowExecutions/exec-1/steps/step-doc-1');
expect(result.current.data).toEqual(stepResponse);
});

it('should set staleTime to Infinity for terminal step status', async () => {
const { result } = renderHook(
() => useStepExecution('exec-1', 'step-doc-1', ExecutionStatus.COMPLETED),
{ wrapper: createWrapper(queryClient) }
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));

const cachedQuery = queryClient.getQueryCache().findAll({
queryKey: ['stepExecution', 'exec-1', 'step-doc-1'],
})[0];
expect(cachedQuery.state.isInvalidated).toBe(false);
expect(cachedQuery.state.dataUpdateCount).toBe(1);

// After initial fetch, no refetch should happen even after the polling interval
mockHttpGet.mockClear();
jest.advanceTimersByTime(10_000);
expect(mockHttpGet).not.toHaveBeenCalled();
});

it('should poll for non-terminal step status', async () => {
const { result } = renderHook(
() => useStepExecution('exec-1', 'step-doc-1', ExecutionStatus.RUNNING),
{ wrapper: createWrapper(queryClient) }
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(mockHttpGet).toHaveBeenCalledTimes(1);

// Advance past the 5s refetch interval — should trigger another fetch
mockHttpGet.mockClear();
jest.advanceTimersByTime(5_000);
await waitFor(() => expect(mockHttpGet).toHaveBeenCalled());
});

it('should stop polling when step transitions to terminal status', async () => {
const { result, rerender } = renderHook(
({ status }: { status: ExecutionStatus }) => useStepExecution('exec-1', 'step-doc-1', status),
{
wrapper: createWrapper(queryClient),
initialProps: { status: ExecutionStatus.RUNNING },
}
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));

// Transition to terminal
rerender({ status: ExecutionStatus.COMPLETED });

mockHttpGet.mockClear();
jest.advanceTimersByTime(15_000);
expect(mockHttpGet).not.toHaveBeenCalled();
});

it('should use the correct query key structure', async () => {
renderHook(() => useStepExecution('exec-1', 'step-doc-1', ExecutionStatus.COMPLETED), {
wrapper: createWrapper(queryClient),
});

await waitFor(() =>
expect(
queryClient.getQueryCache().findAll({
queryKey: ['stepExecution', 'exec-1', 'step-doc-1'],
})
).toHaveLength(1)
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,35 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useQuery } from '@kbn/react-query';
import type { EsWorkflowStepExecution } from '@kbn/workflows';
import type { EsWorkflowStepExecution, ExecutionStatus } from '@kbn/workflows';
import { isTerminalStatus } from '@kbn/workflows';
import { useKibana } from '../../../hooks/use_kibana';

export function useStepExecution(workflowExecutionId: string, stepExecutionId: string) {
const REFETCH_INTERVAL_MS = 5000;

/**
* Fetches a single step execution with full data (input/output).
* Polls while the step is still running, stops once it reaches a terminal status.
*/
export function useStepExecution(
workflowExecutionId: string,
stepExecutionId: string | undefined,
stepStatus: ExecutionStatus | undefined
) {
const { http } = useKibana().services;
const isStepFinished = stepStatus ? isTerminalStatus(stepStatus) : false;

return useQuery({
queryKey: ['stepExecution', workflowExecutionId, stepExecutionId],
queryFn: async () => {
const response = await http?.get<EsWorkflowStepExecution>(
const response = await http.get<EsWorkflowStepExecution>(
`/api/workflowExecutions/${workflowExecutionId}/steps/${stepExecutionId}`
);
return response;
},
enabled: !!workflowExecutionId && !!stepExecutionId,
staleTime: 5000, // Refresh every 5 seconds for real-time logs
refetchInterval: 5000, // Auto-refresh logs
staleTime: isStepFinished ? Infinity : REFETCH_INTERVAL_MS, // will be cleared when switching to a different execution
refetchInterval: isStepFinished ? false : REFETCH_INTERVAL_MS,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* 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 { render } from '@testing-library/react';
import React from 'react';
import { useQueryClient } from '@kbn/react-query';
import type { WorkflowExecutionDto, WorkflowYaml } from '@kbn/workflows';
import { ExecutionStatus } from '@kbn/workflows';
import { WorkflowExecutionDetail } from './workflow_execution_detail';

jest.mock('@kbn/react-query', () => ({
...jest.requireActual('@kbn/react-query'),
useQueryClient: jest.fn(),
}));
const mockUseQueryClient = useQueryClient as jest.MockedFunction<typeof useQueryClient>;

jest.mock('./workflow_execution_panel', () => ({
WorkflowExecutionPanel: () => <div data-test-subj="execution-panel" />,
}));

jest.mock('./workflow_step_execution_details', () => ({
WorkflowStepExecutionDetails: () => <div data-test-subj="step-details" />,
}));

jest.mock('../model/use_step_execution', () => ({
useStepExecution: jest.fn(() => ({ data: undefined, isLoading: false })),
}));

const mockSetSelectedStepExecution = jest.fn();
jest.mock('../../../hooks/use_workflow_url_state', () => ({
useWorkflowUrlState: jest.fn(() => ({
activeTab: 'executions',
setSelectedStepExecution: mockSetSelectedStepExecution,
selectedStepExecutionId: '__overview',
})),
}));

const createMockExecution = (id: string): WorkflowExecutionDto => ({
spaceId: 'default',
id,
status: ExecutionStatus.COMPLETED,
error: null,
isTestRun: false,
startedAt: '2024-01-01T00:00:00Z',
finishedAt: '2024-01-01T00:01:00Z',
workflowId: 'workflow-1',
workflowName: 'Test Workflow',
workflowDefinition: {
version: '1',
name: 'test',
enabled: true,
triggers: [],
steps: [],
} as WorkflowYaml,
stepId: undefined,
stepExecutions: [],
duration: 60000,
triggeredBy: 'manual',
yaml: 'version: "1"',
});

const mockUseWorkflowExecutionPolling = jest.fn((_executionId: string) => ({
workflowExecution: createMockExecution('exec-1'),
isLoading: false,
error: null,
}));
jest.mock('../../../entities/workflows/model/use_workflow_execution_polling', () => ({
useWorkflowExecutionPolling: (executionId: string) =>
mockUseWorkflowExecutionPolling(executionId),
}));

describe('WorkflowExecutionDetail - cache invalidation', () => {
let mockRemoveQueries: jest.Mock;

beforeEach(() => {
jest.clearAllMocks();
mockRemoveQueries = jest.fn();
mockUseQueryClient.mockReturnValue({
removeQueries: mockRemoveQueries,
} as any);
});

it('should call removeQueries on unmount with the current execution query key', () => {
const { unmount } = render(
<WorkflowExecutionDetail executionId="exec-1" onClose={jest.fn()} />
);

expect(mockRemoveQueries).not.toHaveBeenCalled();

unmount();

expect(mockRemoveQueries).toHaveBeenCalledWith({
queryKey: ['stepExecution', 'exec-1'],
});
});

it('should call removeQueries for the previous execution when executionId changes', () => {
const { rerender } = render(
<WorkflowExecutionDetail executionId="exec-1" onClose={jest.fn()} />
);

expect(mockRemoveQueries).not.toHaveBeenCalled();

mockUseWorkflowExecutionPolling.mockReturnValue({
workflowExecution: createMockExecution('exec-2'),
isLoading: false,
error: null,
});

rerender(<WorkflowExecutionDetail executionId="exec-2" onClose={jest.fn()} />);

expect(mockRemoveQueries).toHaveBeenCalledWith({
queryKey: ['stepExecution', 'exec-1'],
});
});
});
Loading
Loading