Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@

import type { UseEuiTheme } from '@elastic/eui';
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { css } from '@emotion/react';
import { useMemoCss } from '@kbn/css-utils/public/use_memo_css';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { WORKFLOWS_UI_VISUAL_EDITOR_SETTING_ID } from '@kbn/workflows';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { css } from '@emotion/react';
import { useMemoCss } from '@kbn/css-utils/public/use_memo_css';
import { useWorkflowActions } from '../../../entities/workflows/model/use_workflow_actions';
import { useWorkflowDetail } from '../../../entities/workflows/model/useWorkflowDetail';
import { useWorkflowExecution } from '../../../entities/workflows/model/useWorkflowExecution';
import { TestWorkflowModal } from '../../../features/run_workflow/ui/test_workflow_modal';
import { WorkflowEventModal } from '../../../features/run_workflow/ui/workflow_event_modal';
import { WorkflowExecutionDetail } from '../../../features/workflow_execution_detail';
import { WorkflowExecutionList } from '../../../features/workflow_execution_list/ui/workflow_execution_list_stateful';
import { useWorkflowUrlState } from '../../../hooks/use_workflow_url_state';
import { WorkflowExecutionDetail } from '../../../features/workflow_execution_detail';
import { useWorkflowExecution } from '../../../entities/workflows/model/useWorkflowExecution';
import { WorkflowDetailHeader } from './workflow_detail_header';
import { useWorkflowActions } from '../../../entities/workflows/model/use_workflow_actions';
import { WorkflowEventModal } from '../../../features/run_workflow/ui/workflow_event_modal';
import { TestWorkflowModal } from '../../../features/run_workflow/ui/test_workflow_modal';

const WorkflowYAMLEditor = React.lazy(() =>
import('../../../widgets/workflow_yaml_editor').then((module) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,71 @@

import type { UseEuiTheme } from '@elastic/eui';
import { EuiIcon, useEuiTheme } from '@elastic/eui';
import { monaco } from '@kbn/monaco';
import type { EsWorkflowStepExecution } from '@kbn/workflows';
import { getJsonSchemaFromYamlSchema } from '@kbn/workflows';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { css } from '@emotion/react';
import { useMemoCss } from '@kbn/css-utils/public/use_memo_css';
import YAML from 'yaml';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react';
import { monaco } from '@kbn/monaco';
import type { EsWorkflowStepExecution } from '@kbn/workflows';
import { getJsonSchemaFromYamlSchema } from '@kbn/workflows';
import type { SchemasSettings } from 'monaco-yaml';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import YAML, { isPair, isScalar, visit } from 'yaml';
import { getStepNode } from '../../../../common/lib/yaml_utils';
import { WORKFLOW_ZOD_SCHEMA, WORKFLOW_ZOD_SCHEMA_LOOSE } from '../../../../common/schema';
import { UnsavedChangesPrompt } from '../../../shared/ui/unsaved_changes_prompt';
import { YamlEditor } from '../../../shared/ui/yaml_editor';
import { getCompletionItemProvider } from '../lib/get_completion_item_provider';
import { useYamlValidation } from '../lib/use_yaml_validation';
import {
getHighlightStepDecorations,
getMonacoRangeFromYamlNode,
navigateToErrorPosition,
} from '../lib/utils';
import { WorkflowYAMLValidationErrors } from './workflow_yaml_validation_errors';
import { getCompletionItemProvider } from '../lib/get_completion_item_provider';
import { getStepNode } from '../../../../common/lib/yaml_utils';
import { UnsavedChangesPrompt } from '../../../shared/ui/unsaved_changes_prompt';
import type { YamlValidationError } from '../model/types';
import { WorkflowYAMLValidationErrors } from './workflow_yaml_validation_errors';

const getTriggerNodes = (
yamlDocument: YAML.Document
): Array<{ node: any; triggerType: string; typePair: any }> => {
const triggerNodes: Array<{ node: any; triggerType: string; typePair: any }> = [];

if (!yamlDocument?.contents) return triggerNodes;

visit(yamlDocument, {
Pair(key, pair, ancestors) {
if (!pair.key || !isScalar(pair.key) || pair.key.value !== 'type') {
return;
}

// Check if this is a type field within a trigger
const path = ancestors.slice();
let isTriggerType = false;

// Walk up the ancestors to see if we're in a triggers array
for (let i = path.length - 1; i >= 0; i--) {
const ancestor = path[i];
if (isPair(ancestor) && isScalar(ancestor.key) && ancestor.key.value === 'triggers') {
isTriggerType = true;
break;
}
}

if (isTriggerType && isScalar(pair.value)) {
const triggerType = pair.value.value as string;
// Find the parent map node that contains this trigger
const triggerMapNode = ancestors[ancestors.length - 1];
triggerNodes.push({
node: triggerMapNode,
triggerType,
typePair: pair, // Store the actual type pair for precise positioning
});
}
},
});

return triggerNodes;
};

const WorkflowSchemaUri = 'file:///workflow-schema.json';

Expand Down Expand Up @@ -91,6 +134,8 @@ export const WorkflowYAMLEditor = ({
useRef<monaco.editor.IEditorDecorationsCollection | null>(null);
const stepExecutionsDecorationCollectionRef =
useRef<monaco.editor.IEditorDecorationsCollection | null>(null);
const alertTriggerDecorationCollectionRef =
useRef<monaco.editor.IEditorDecorationsCollection | null>(null);

const {
error: errorValidating,
Expand Down Expand Up @@ -235,6 +280,103 @@ export const WorkflowYAMLEditor = ({
editorRef.current?.createDecorationsCollection(decorations) ?? null;
}, [isEditorMounted, stepExecutions, highlightStep, yamlDocument]);

useEffect(() => {
const model = editorRef.current?.getModel() ?? null;
if (alertTriggerDecorationCollectionRef.current) {
// clear existing decorations
alertTriggerDecorationCollectionRef.current.clear();
}

// Don't show alert dots when in executions view
if (!model || !yamlDocument || !isEditorMounted || readOnly) {
return;
}

const triggerNodes = getTriggerNodes(yamlDocument);
const alertTriggers = triggerNodes.filter(({ triggerType }) => triggerType === 'alert');

if (alertTriggers.length === 0) {
return;
}

const decorations = alertTriggers
.map(({ node, typePair }) => {
// Try to get the range from the typePair first, fallback to searching within the trigger node
let typeRange = getMonacoRangeFromYamlNode(model, typePair);

if (!typeRange) {
// Fallback: use the trigger node range and search for the type line
const triggerRange = getMonacoRangeFromYamlNode(model, node);
if (!triggerRange) {
return null;
}

// Find the specific line that contains "type:" and "alert" within this trigger
let typeLineNumber = triggerRange.startLineNumber;
for (
let lineNum = triggerRange.startLineNumber;
lineNum <= triggerRange.endLineNumber;
lineNum++
) {
const lineContent = model.getLineContent(lineNum);
if (lineContent.includes('type:') && lineContent.includes('alert')) {
typeLineNumber = lineNum;
break;
}
}

typeRange = {
startLineNumber: typeLineNumber,
endLineNumber: typeLineNumber,
startColumn: 1,
endColumn: model.getLineMaxColumn(typeLineNumber),
};
}

const glyphDecoration: monaco.editor.IModelDeltaDecoration = {
range: new monaco.Range(
typeRange.startLineNumber,
1,
typeRange.startLineNumber,
model.getLineMaxColumn(typeRange.startLineNumber)
),
options: {
glyphMarginClassName: 'alert-trigger-glyph',
glyphMarginHoverMessage: {
value: i18n.translate(
'workflows.workflowDetail.yamlEditor.alertTriggerGlyphTooltip',
{
defaultMessage:
'Alert trigger: This workflow will be executed automatically only when connected to a rule via the "Run Workflow" action.',
}
),
},
},
};

const lineHighlightDecoration: monaco.editor.IModelDeltaDecoration = {
range: new monaco.Range(
typeRange.startLineNumber,
1,
typeRange.startLineNumber,
model.getLineMaxColumn(typeRange.startLineNumber)
),
options: {
className: 'alert-trigger-highlight',
marginClassName: 'alert-trigger-highlight',
isWholeLine: true,
},
};

return [glyphDecoration, lineHighlightDecoration];
})
.flat()
.filter((d) => d !== null) as monaco.editor.IModelDeltaDecoration[];

alertTriggerDecorationCollectionRef.current =
editorRef.current?.createDecorationsCollection(decorations) ?? null;
}, [isEditorMounted, yamlDocument, readOnly]);

const completionProvider = useMemo(() => {
return getCompletionItemProvider(WORKFLOW_ZOD_SCHEMA_LOOSE);
}, []);
Expand Down Expand Up @@ -471,6 +613,19 @@ const componentStyles = {
borderRadius: '50%',
},
},
'.alert-trigger-glyph': {
'&:before': {
content: '""',
display: 'block',
width: '12px',
height: '12px',
backgroundColor: euiTheme.colors.warning,
borderRadius: '50%',
},
},
'.alert-trigger-highlight': {
backgroundColor: euiTheme.colors.backgroundLightWarning,
},
}),
editorContainer: css({
flex: '1 1 0',
Expand Down