Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -154,7 +154,7 @@ describe('WorkflowExecuteModal', () => {
expect(getByText('Run Workflow')).toBeInTheDocument();
});

it('renders all trigger type buttons', () => {
it('renders all trigger type buttons when the workflow definition is missing', () => {
const { getByText } = renderWithProviders(
<WorkflowExecuteModal
isTestRun={false}
Expand All @@ -171,6 +171,26 @@ describe('WorkflowExecuteModal', () => {
expect(getByText('Historical')).toBeInTheDocument();
});

it('shows only tabs that match triggers declared in the workflow', () => {
const { getByText, queryByText } = renderWithProviders(
<WorkflowExecuteModal
isTestRun={false}
definition={{
...baseWorkflowDefinition,
triggers: [{ type: 'alert' }],
}}
onClose={mockOnClose}
onSubmit={mockOnSubmit}
/>
);

expect(getByText('Alert')).toBeInTheDocument();
expect(getByText('Manual')).toBeInTheDocument();
expect(getByText('Historical')).toBeInTheDocument();
expect(queryByText('Document')).not.toBeInTheDocument();
expect(queryByText('Event')).not.toBeInTheDocument();
});

it('uses the test run title and still exposes the full trigger tab set', () => {
const { getByText } = renderWithProviders(
<WorkflowExecuteModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { FormattedMessage } from '@kbn/i18n-react';
import type { WorkflowYaml } from '@kbn/workflows';
import { extractNormalizedInputsFromYaml } from '@kbn/workflows/spec/lib/field_conversion';
import { useWorkflowsCapabilities } from '@kbn/workflows-ui';
import { ENABLED_TRIGGER_TABS } from './constants';
import { TRIGGER_TABS_DESCRIPTIONS, TRIGGER_TABS_LABELS } from './translations';
import type { WorkflowTriggerTab } from './types';
import { WorkflowExecuteAlertForm } from './workflow_execute_alert_form';
Expand All @@ -41,7 +40,9 @@ import { WorkflowExecuteIndexForm } from './workflow_execute_index_form';
import { WorkflowExecuteManualForm } from './workflow_execute_manual_form';
import { getWorkflowExecuteModalGlobalStyles } from './workflow_execute_modal_global_styles';
import {
ensureSelectedTriggerTabVisible,
getFallbackTriggerTab,
getVisibleWorkflowTriggerTabs,
hasCustomEventTrigger,
hasWorkflowInputFields,
isRacAlertsApiForbiddenError,
Expand Down Expand Up @@ -92,6 +93,11 @@ export const WorkflowExecuteModal = React.memo<WorkflowExecuteModalProps>(
[definition, yamlString]
);

const visibleTriggerTabs = useMemo(
() => getVisibleWorkflowTriggerTabs(definition),
[definition]
);

const [selectedTrigger, setSelectedTrigger] = useState<WorkflowTriggerTab>(() =>
resolveInitialSelectedTrigger(
definition,
Expand Down Expand Up @@ -213,23 +219,26 @@ export const WorkflowExecuteModal = React.memo<WorkflowExecuteModalProps>(

useEffect(() => {
setSelectedTrigger((current) => {
let next = current;
if (current === 'alert' && !hasAlertRacAccess) {
return getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
}
if (current === 'historical' && !canReadWorkflowExecution) {
return getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
}
if (current === 'event' && (!canReadWorkflowExecution || !eventDrivenExecutionEnabled)) {
return getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
next = getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
} else if (current === 'historical' && !canReadWorkflowExecution) {
next = getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
} else if (
current === 'event' &&
(!canReadWorkflowExecution || !eventDrivenExecutionEnabled)
) {
next = getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
}
return current;
return ensureSelectedTriggerTabVisible(next, visibleTriggerTabs);
});
}, [
hasAlertRacAccess,
canReadWorkflowExecution,
eventDrivenExecutionEnabled,
normalizedInputs,
definition,
visibleTriggerTabs,
]);

if (shouldAutoRun) {
Expand Down Expand Up @@ -363,7 +372,7 @@ export const WorkflowExecuteModal = React.memo<WorkflowExecuteModalProps>(
`}
>
<EuiFlexGroup direction="row" gutterSize="s" alignItems="stretch">
{ENABLED_TRIGGER_TABS.map((trigger) => {
{visibleTriggerTabs.map((trigger) => {
let triggerDisabledTooltip: string | undefined;
if (trigger === 'alert' && !hasAlertRacAccess) {
triggerDisabledTooltip = alertTabDisabledTooltip;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
buildDefaultTriggerEventSearchQuery,
buildWorkflowTriggerScopeKql,
getFallbackTriggerTab,
getVisibleWorkflowTriggerTabs,
getWorkflowCustomTriggerTypeIds,
hasCustomEventTrigger,
resolveInitialSelectedTrigger,
Expand Down Expand Up @@ -139,6 +140,36 @@ describe('buildDefaultTriggerEventSearchQuery', () => {
});
});

describe('getVisibleWorkflowTriggerTabs', () => {
it('returns all tabs when the workflow has no triggers', () => {
expect(getVisibleWorkflowTriggerTabs(null)).toEqual([
'alert',
'index',
'event',
'manual',
'historical',
]);
});

it('returns alert, manual, and historical for alert-only workflows', () => {
expect(
getVisibleWorkflowTriggerTabs({ ...baseDefinition, triggers: [{ type: 'alert' }] })
).toEqual(['alert', 'manual', 'historical']);
});

it('returns document, manual, and historical for manual-only workflows', () => {
expect(
getVisibleWorkflowTriggerTabs({ ...baseDefinition, triggers: [{ type: 'manual' }] })
).toEqual(['index', 'manual', 'historical']);
});

it('returns event, manual, and historical for custom event-driven workflows', () => {
expect(
getVisibleWorkflowTriggerTabs(workflowWithExtensionTriggers([{ type: 'cases.created' }]))
).toEqual(['event', 'manual', 'historical']);
});
});

describe('getFallbackTriggerTab', () => {
const normalizedWithOneField: NormalizedWorkflowInputs = normalizeFieldsToJsonSchema([
{ name: 'x', type: 'string', required: true },
Expand Down Expand Up @@ -174,9 +205,9 @@ describe('resolveInitialSelectedTrigger', () => {
);
});

it('falls back when custom triggers exist but execution read is denied', () => {
it('falls back to the first visible tab when custom triggers exist but execution read is denied', () => {
expect(resolveInitialSelectedTrigger(customOnly, undefined, true, false, undefined)).toBe(
'index'
'event'
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { WorkflowYaml } from '@kbn/workflows';
import { isTriggerType } from '@kbn/workflows';
import { getInputsFromDefinition } from '@kbn/workflows/spec/lib/field_conversion';
import type { JsonModelSchemaType } from '@kbn/workflows/spec/schema/common/json_model_schema';
import { ENABLED_TRIGGER_TABS } from './constants';
import type { WorkflowTriggerTab } from './types';

export type NormalizedWorkflowInputs = JsonModelSchemaType | undefined;
Expand Down Expand Up @@ -52,6 +53,47 @@ export function isRacAlertsApiForbiddenError(error: unknown): boolean {
);
}

export function workflowDefinitionHasTriggerType(
definition: WorkflowYaml | null,
triggerType: string
): boolean {
return Boolean(definition?.triggers?.some((trigger) => trigger.type === triggerType));
}

/** Run-modal tabs to show based on triggers declared in the workflow definition. */
export function getVisibleWorkflowTriggerTabs(
definition: WorkflowYaml | null
): readonly WorkflowTriggerTab[] {
if (!definition?.triggers?.length) {
return ENABLED_TRIGGER_TABS;
}

const visible: WorkflowTriggerTab[] = [];

if (workflowDefinitionHasTriggerType(definition, 'alert')) {
visible.push('alert');
}
if (hasCustomEventTrigger(definition)) {
visible.push('event');
}
if (workflowDefinitionHasTriggerType(definition, 'manual')) {
visible.push('index');
}
visible.push('manual', 'historical');

return visible;
}

export function ensureSelectedTriggerTabVisible(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor flagged this as a possible issue -

ensureSelectedTriggerTabVisible falls back to the first visible tab without checking whether that tab is disabled.

With the new filtering, this can leave users on a disabled selected tab. For example, an alert-only workflow with no RAC access falls back from hidden index to visible alert, but alert is disabled. Similarly, a custom-event-only workflow without canReadWorkflowExecution falls back to hidden index, then gets coerced back to visible event, even though the modal disables Event. The helper test currently codifies that bad outcome by expecting event when read execution is denied.

With an alert-only workflow, the visible tabs are alert, manual, and historical. If the RAC prefetch returns 403, alert becomes disabled, but the fallback can still land back on it: getFallbackTriggerTab() returns hidden index, then ensureSelectedTriggerTabVisible() coerces that to the first visible tab, alert. Can we make the fallback choose the first visible enabled tab instead, e.g. manual in this case?

selected: WorkflowTriggerTab,
visibleTabs: readonly WorkflowTriggerTab[]
): WorkflowTriggerTab {
if (visibleTabs.includes(selected)) {
return selected;
}
return visibleTabs[0] ?? 'historical';
}

export function hasCustomEventTrigger(definition: WorkflowYaml | null): boolean {
if (!definition?.triggers?.length) {
return false;
Expand Down Expand Up @@ -147,36 +189,36 @@ export function resolveInitialSelectedTrigger(
canReadWorkflowExecution: boolean,
normalizedInputs: NormalizedWorkflowInputs | undefined
): WorkflowTriggerTab {
if (initialExecutionId) {
return canReadWorkflowExecution
? 'historical'
: getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
}
const visibleTabs = getVisibleWorkflowTriggerTabs(definition);

const hasAlertTrigger = Boolean(definition?.triggers?.some((t) => t.type === 'alert'));
const hasEventTrigger = hasCustomEventTrigger(definition);
let selected: WorkflowTriggerTab;

if (hasAlertTrigger) {
return hasAlertRacAccess
? 'alert'
if (initialExecutionId) {
selected = canReadWorkflowExecution
? 'historical'
: getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
} else {
const hasAlertTrigger = workflowDefinitionHasTriggerType(definition, 'alert');
const hasEventTrigger = hasCustomEventTrigger(definition);

if (hasAlertTrigger) {
selected = hasAlertRacAccess
? 'alert'
: getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
} else if (hasEventTrigger && canReadWorkflowExecution) {
selected = 'event';
} else if (hasEventTrigger && !canReadWorkflowExecution) {
selected = getFallbackTriggerTab(normalizedInputs, definition, false);
} else if (hasWorkflowInputFields(normalizedInputs)) {
selected = getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
} else {
const preferred = getDefaultTrigger(definition);
selected =
preferred === 'alert' && !hasAlertRacAccess
? getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution)
: preferred;
}
}

if (hasEventTrigger && canReadWorkflowExecution) {
return 'event';
}

if (hasEventTrigger && !canReadWorkflowExecution) {
return getFallbackTriggerTab(normalizedInputs, definition, false);
}

if (hasWorkflowInputFields(normalizedInputs)) {
return getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
}

const preferred = getDefaultTrigger(definition);
if (preferred === 'alert' && !hasAlertRacAccess) {
return getFallbackTriggerTab(normalizedInputs, definition, canReadWorkflowExecution);
}
return preferred;
return ensureSelectedTriggerTabVisible(selected, visibleTabs);
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ steps:
message: "Test run: {{ execution.isTestRun }}"
`;

/**
* Workflow with an event-driven trigger so the Run/Test modal shows the Event tab.
*/
export const getTestRunEventTabWorkflowYaml = (name: string) => `
name: ${name}
enabled: false
description: Workflow with workflows.failed trigger for Event tab scout test
triggers:
- type: workflows.failed
steps:
- name: hello_world_step
type: console
with:
message: "Test run: {{ execution.isTestRun }}"
`;

/**
* Workflow with a foreach loop (4 items) and a nested console step.
* Used for individual step run and context override tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { getCreateGetUpdateCaseWorkflowYaml } from './create_get_update_case';
export {
getListTestWorkflowYaml,
getTestRunWorkflowYaml,
getTestRunEventTabWorkflowYaml,
getWorkflowWithLoopYaml,
getIterationLoopWorkflowYaml,
getManyIterationsWorkflowYaml,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { expect } from '@kbn/scout/ui';
import { spaceTest as test } from '../../fixtures';
import { cleanupWorkflowsAndRules } from '../../fixtures/cleanup';
import { EXECUTION_TIMEOUT } from '../../fixtures/constants';
import { getTestRunWorkflowYaml, getWorkflowWithLoopYaml } from '../../fixtures/workflows';
import {
getTestRunEventTabWorkflowYaml,
getTestRunWorkflowYaml,
getWorkflowWithLoopYaml,
} from '../../fixtures/workflows';
import { getWorkflowWithEventInputYaml } from '../../fixtures/workflows/console_workflows';

test.describe('Workflow execution - Test runs', { tag: [...tags.stateful.classic] }, () => {
Expand Down Expand Up @@ -56,7 +60,9 @@ test.describe('Workflow execution - Test runs', { tag: [...tags.stateful.classic
const workflowName = 'Test Workflow Event Tab From Editor';

await pageObjects.workflowEditor.gotoNewWorkflow();
await pageObjects.workflowEditor.setYamlEditorValue(getTestRunWorkflowYaml(workflowName));
await pageObjects.workflowEditor.setYamlEditorValue(
getTestRunEventTabWorkflowYaml(workflowName)
);
await pageObjects.workflowEditor.saveWorkflow();

await pageObjects.workflowEditor.clickRunButton();
Expand Down
Loading