Skip to content
Merged
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 @@ -51,11 +51,17 @@ export const selectWorkflowDefinition = createSelector(
(computed) => computed?.workflowDefinition
);

// Only checks if the current workflow yaml can be parses, does check the schema, only the yaml syntax
// Only checks if the current workflow yaml can be parsed, does not check the schema, only the yaml syntax
export const selectIsYamlSyntaxValid = createSelector(selectYamlComputed, (computed): boolean =>
Boolean(computed?.workflowDefinition)
);

// Checks whether validation errors (from strict schema + custom validations) are present
export const selectHasYamlSchemaValidationErrors = createSelector(
selectDetail,
(detail): boolean => detail.hasYamlSchemaValidationErrors
);

export const selectFocusedStepId = createSelector(selectDetail, (detail) => detail.focusedStepId);

export const selectHighlightedStepId = createSelector(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const initialState: WorkflowDetailState = {
highlightedStepId: undefined,
isTestModalOpen: false,
loading: initialLoadingState,
hasYamlSchemaValidationErrors: false,
connectorFlyout: {
isOpen: false,
connectorType: undefined,
Expand Down Expand Up @@ -89,6 +90,10 @@ const workflowDetailSlice = createSlice({
state.activeTab = action.payload;
},

setHasYamlSchemaValidationErrors: (state, action: { payload: boolean }) => {
state.hasYamlSchemaValidationErrors = action.payload;
},

// Connector flyout actions
openCreateConnectorFlyout: (
state,
Expand Down Expand Up @@ -151,6 +156,7 @@ export const {
setExecution,
clearExecution,
setActiveTab,
setHasYamlSchemaValidationErrors,
openCreateConnectorFlyout,
openEditConnectorFlyout,
closeConnectorFlyout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export interface WorkflowDetailState {
schema: WorkflowZodSchemaType;
/** Loading states for async operations */
loading: LoadingStates;
/** Whether the editor has validation errors (strict schema + custom validations) */
hasYamlSchemaValidationErrors: boolean;
/** Connector flyout state */
connectorFlyout: {
isOpen: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import type { WorkflowDetailHeaderProps } from './workflow_detail_header';
import { WorkflowDetailHeader } from './workflow_detail_header';
import { createMockStore } from '../../../entities/workflows/store/__mocks__/store.mock';
import {
_setComputedDataInternal,
_clearComputedData,
setHasYamlSchemaValidationErrors,
setWorkflow,
setYamlString,
} from '../../../entities/workflows/store/workflow_detail/slice';
Expand Down Expand Up @@ -72,27 +73,33 @@ describe('WorkflowDetailHeader', () => {

const renderWithProviders = (
component: React.ReactElement,
{ isValid = true, hasChanges = false }: { isValid?: boolean; hasChanges?: boolean } = {}
{
isValid = true,
hasChanges = false,
hasYamlSchemaValidationErrors = false,
serverValid = true,
}: {
isValid?: boolean;
hasChanges?: boolean;
hasYamlSchemaValidationErrors?: boolean;
serverValid?: boolean;
} = {}
) => {
const store = createMockStore();

// Set up the workflow in the store
store.dispatch(setWorkflow(mockWorkflow));
// Set up the workflow in the store (with server-side valid flag)
store.dispatch(setWorkflow({ ...mockWorkflow, valid: serverValid }));
store.dispatch(setYamlString(hasChanges ? 'modified yaml' : mockWorkflow.yaml));

// Set computed data to control syntax validation
if (isValid) {
store.dispatch(
_setComputedDataInternal({
workflowDefinition: {
version: '1',
name: 'Test Workflow',
enabled: true,
triggers: [],
steps: [],
},
})
);
if (!isValid) {
// Clear the computed data that the middleware auto-generated from the yaml,
// so that selectIsYamlSyntaxValid returns false
store.dispatch(_clearComputedData());
}

// Simulate strict validation errors from Monaco
if (hasYamlSchemaValidationErrors) {
store.dispatch(setHasYamlSchemaValidationErrors(true));
}

const wrapper = ({ children }: { children: React.ReactNode }) => {
Expand Down Expand Up @@ -158,15 +165,39 @@ describe('WorkflowDetailHeader', () => {
expect(container).toBeTruthy();
});

// We shouldn't rely on parseResult to determine if the yaml is valid now
// instead we should move validationErrors to the store and use it to determine it
it.skip('disables run workflow button when yaml is invalid', () => {
it('disables run workflow button when yaml has syntax errors', () => {
const result = renderWithProviders(<WorkflowDetailHeader {...defaultProps} />, {
isValid: false,
});
expect(result.getByTestId('runWorkflowHeaderButton')).toBeDisabled();
});

it('disables run workflow button when yaml has validation errors', () => {
const result = renderWithProviders(<WorkflowDetailHeader {...defaultProps} />, {
isValid: true,
hasYamlSchemaValidationErrors: true,
});
expect(result.getByTestId('runWorkflowHeaderButton')).toBeDisabled();
});

it('disables enabled toggle when yaml has validation errors', () => {
const result = renderWithProviders(<WorkflowDetailHeader {...defaultProps} />, {
isValid: true,
hasYamlSchemaValidationErrors: true,
});
const toggle = result.getByRole('switch');
expect(toggle).toBeDisabled();
});

it('disables enabled toggle when server reports workflow as invalid (e.g. initial page load)', () => {
const result = renderWithProviders(<WorkflowDetailHeader {...defaultProps} />, {
isValid: true,
serverValid: false,
});
const toggle = result.getByRole('switch');
expect(toggle).toBeDisabled();
});

it('enables run workflow button when yaml is valid', () => {
const result = renderWithProviders(<WorkflowDetailHeader {...defaultProps} />);
const button = result.getByTestId('runWorkflowHeaderButton');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { useSaveYaml } from '../../../entities/workflows/model/use_save_yaml';
import { useUpdateWorkflow } from '../../../entities/workflows/model/use_update_workflow';
import {
selectHasChanges,
selectHasYamlSchemaValidationErrors,
selectIsExecutionsTab,
selectIsSavingYaml,
selectIsYamlSynced,
Expand Down Expand Up @@ -105,6 +106,7 @@ export const WorkflowDetailHeader = React.memo(

const workflow = useSelector(selectWorkflow);
const isSyntaxValid = useSelector(selectIsYamlSyntaxValid);
const hasYamlSchemaValidationErrors = useSelector(selectHasYamlSchemaValidationErrors);
const hasUnsavedChanges = useSelector(selectHasChanges);
const isExecutionsTab = useSelector(selectIsExecutionsTab);
const isYamlSynced = useSelector(selectIsYamlSynced);
Expand Down Expand Up @@ -135,13 +137,17 @@ export const WorkflowDetailHeader = React.memo(

const [showRunConfirmation, setShowRunConfirmation] = useState(false);

// Combined validity: syntax must parse AND no strict validation errors AND server considers it valid.
// workflow?.valid !== false covers the initial page load before Monaco validates.
const isValid = isSyntaxValid && !hasYamlSchemaValidationErrors && workflow?.valid !== false;

const runWorkflowTooltipContent = useMemo(() => {
return getTestRunTooltipContent({
isExecutionsTab,
isValid: isSyntaxValid,
isValid,
canRunWorkflow: canExecuteWorkflow,
});
}, [isSyntaxValid, canExecuteWorkflow, isExecutionsTab]);
}, [isValid, canExecuteWorkflow, isExecutionsTab]);

const saveWorkflowTooltipContent = useMemo(() => {
const isCreate = !workflowId;
Expand Down Expand Up @@ -262,9 +268,9 @@ export const WorkflowDetailHeader = React.memo(
? i18n.translate('workflows.workflowDetailHeader.unsaved', {
defaultMessage: 'Save changes to enable/disable workflow',
})
: !isSyntaxValid
: !isValid
? i18n.translate('workflows.workflowDetailHeader.invalid', {
defaultMessage: 'Fix errors to enable workflow',
defaultMessage: 'Fix validation errors to enable workflow',
})
: undefined
}
Expand All @@ -274,7 +280,7 @@ export const WorkflowDetailHeader = React.memo(
!workflowId ||
isLoading ||
!canUpdateWorkflow ||
!isSyntaxValid ||
!isValid ||
hasUnsavedChanges
}
checked={isEnabled}
Expand All @@ -292,7 +298,7 @@ export const WorkflowDetailHeader = React.memo(
iconType="play"
size="s"
onClick={handleRunClickWithUnsavedCheck}
disabled={isExecutionsTab || !canExecuteWorkflow || isLoading || !isSyntaxValid}
disabled={isExecutionsTab || !canExecuteWorkflow || isLoading || !isValid}
aria-label={Translations.runWorkflow}
data-test-subj="runWorkflowHeaderButton"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ import {
selectStepExecutions,
selectWorkflow,
} from '../../../entities/workflows/store/workflow_detail/selectors';
import { setIsTestModalOpen } from '../../../entities/workflows/store/workflow_detail/slice';
import {
setHasYamlSchemaValidationErrors,
setIsTestModalOpen,
} from '../../../entities/workflows/store/workflow_detail/slice';
import { ActionsMenuPopover } from '../../../features/actions_menu_popover';
import type { ActionOptionData } from '../../../features/actions_menu_popover/types';
import { useMonacoMarkersChangedInterceptor } from '../../../features/validate_workflow_yaml/lib/use_monaco_markers_changed_interceptor';
Expand Down Expand Up @@ -256,6 +259,12 @@ export const WorkflowYAMLEditor = ({
workflowYamlSchema: workflowYamlSchema as z.ZodSchema,
});

// Sync validation error state to Redux so sibling components (e.g. header toggle) can react
useEffect(() => {
const hasErrors = validationErrors.some((e) => e.severity === 'error');
dispatch(setHasYamlSchemaValidationErrors(hasErrors));
}, [validationErrors, dispatch]);

const handleErrorClick = useCallback((error: YamlValidationResult) => {
if (!editorRef.current) {
return;
Expand Down
Loading