Skip to content

Commit

Permalink
feat: Adding incident workflow triggers (#1861)
Browse files Browse the repository at this point in the history
  • Loading branch information
VladimirFilonov authored Sep 19, 2024
1 parent 91d0a9a commit 286e303
Show file tree
Hide file tree
Showing 23 changed files with 515 additions and 92 deletions.
13 changes: 12 additions & 1 deletion docs/workflows/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ In this section we will review the Workflow components.
## Triggers
<Tip>When you run alert with the CLI using `keep run`, the CLI run the alert regardless of the triggers.</Tip>
A trigger is an event that starts the workflow. It could be a manual trigger, an alert, or an interval depending on your use case.
Keep support three types of triggers:
Keep support four types of triggers:
### Manual trigger
```
# run manually
Expand All @@ -28,6 +28,17 @@ triggers:
value: cloudwatch
```

### Incident trigger
```
# run when incident get created, update or deleted
# You can use multiple events, but at least one is required
triggers:
- type: incident
events:
- created
- deleted
```

### Interval trigger
```
# run every 10 seconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ function getTriggerIcon(triggered_by: string) {
case "Manual": return FaHandPointer;
case "Scheduler": return PiDiamondsFourFill;
case "Alert": return HiBellAlert;
case "Incident": return HiBellAlert;
default: return PiDiamondsFourFill;
}
}
Expand Down Expand Up @@ -159,6 +160,9 @@ export function ExecutionTable({
case triggered_by.substring(0, 6) === "manual":
valueToShow = "Manual";
break;
case triggered_by.substring(0, 8) === "incident":
valueToShow = "Incident";
break;
}
}

Expand Down
1 change: 1 addition & 0 deletions keep-ui/app/workflows/builder/CustomNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { toast } from "react-toastify";
function IconUrlProvider(data: FlowNode["data"]) {
const { componentType, type } = data || {};
if (type === "alert" || type === "workflow" || type === "trigger" || !type) return "/keep.png";
if (type === "incident" || type === "workflow" || type === "trigger" || !type) return "/keep.png";
return `/icons/${type
?.replace("step-", "")
?.replace("action-", "")
Expand Down
7 changes: 3 additions & 4 deletions keep-ui/app/workflows/builder/ReactFlowEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const ReactFlowEditor = ({
const [isOpen, setIsOpen] = useState(false);
const stepEditorRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const isTrigger = ['interval', 'manual', 'alert'].includes(selectedNode || '')
const isTrigger = ['interval', 'manual', 'alert', 'incident'].includes(selectedNode || '')
const saveRef = useRef<boolean>(false);
useEffect(()=>{
if(saveRef.current && synced){
Expand All @@ -34,7 +34,6 @@ const ReactFlowEditor = ({
}
}, [saveRef?.current, synced])


useEffect(() => {
setIsOpen(true);
if (selectedNode) {
Expand Down Expand Up @@ -123,11 +122,11 @@ const ReactFlowEditor = ({
</button>
<div className="flex-1 p-2 bg-white border-2 overflow-y-auto">
<div style={{ width: "300px" }}>
<GlobalEditorV2 synced={synced}
<GlobalEditorV2 synced={synced}
saveRef={saveRef}
/>
{!selectedNode?.includes('empty') && !isTrigger && <Divider ref={stepEditorRef} />}
{!selectedNode?.includes('empty') && !isTrigger && <StepEditorV2
{!selectedNode?.includes('empty') && !isTrigger && <StepEditorV2
providers={providers}
installedProviders={installedProviders}
setSynced={setSynced}
Expand Down
3 changes: 2 additions & 1 deletion keep-ui/app/workflows/builder/ToolBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const GroupedMenu = ({ name, steps, searchTerm, isDraggable = true }: {
function IconUrlProvider(data: any) {
const { type } = data || {};
if (type === "alert" || type === "workflow") return "/keep.png";
if (type === "incident" || type === "workflow") return "/keep.png";
return `/icons/${type
?.replace("step-", "")
?.replace("action-", "")
Expand Down Expand Up @@ -126,7 +127,7 @@ const DragAndDropSidebar = ({ isDraggable }: {
setIsVisible(!isDraggable)
}, [selectedNode, selectedEdge, isDraggable]);

const triggerNodeMap = nodes.filter((node: any) => ['interval', 'manual', 'alert'].includes(node?.id)).reduce((obj: any, node: any) => {
const triggerNodeMap = nodes.filter((node: any) => ['interval', 'manual', 'alert', 'incident'].includes(node?.id)).reduce((obj: any, node: any) => {
obj[node.id] = true;
return obj;
}, {} as Record<string, boolean>);
Expand Down
8 changes: 6 additions & 2 deletions keep-ui/app/workflows/builder/builder-store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ function addNodeBetween(nodeOrEdge: string | null, step: V2Step, type: string, s
set({v2Properties: {...get().v2Properties, [newNodeId]: {}}});
break;
}
case "incident": {
set({v2Properties: {...get().v2Properties, [newNodeId]: {}}});
break;
}
}
}

Expand Down Expand Up @@ -437,7 +441,7 @@ const useStore = create<FlowState>((set, get) => ({


finalEdges = edges.filter((edge) => !(idArray.includes(edge.source) || idArray.includes(edge.target)));
if (['interval', 'alert', 'manual'].includes(ids) && edges.some((edge) => edge.source === 'trigger_start' && edge.target !== ids)) {
if (['interval', 'alert', 'manual', 'incident'].includes(ids) && edges.some((edge) => edge.source === 'trigger_start' && edge.target !== ids)) {
edges = edges.filter((edge) => !(idArray.includes(edge.source)));
}
const sources = [...new Set(edges.filter((edge) => startNode.id === edge.target))];
Expand All @@ -457,7 +461,7 @@ const useStore = create<FlowState>((set, get) => ({
const newNode = createDefaultNodeV2({ ...nodes[endIndex + 1].data, islayouted: false }, nodes[endIndex + 1].id);

const newNodes = [...nodes.slice(0, nodeStartIndex), newNode, ...nodes.slice(endIndex + 2)];
if(['manual', 'alert', 'interval'].includes(ids)) {
if(['manual', 'alert', 'interval', 'incident'].includes(ids)) {
const v2Properties = get().v2Properties;
delete v2Properties[ids];
set({ v2Properties });
Expand Down
11 changes: 9 additions & 2 deletions keep-ui/app/workflows/builder/builder-validators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ export function globalValidatorV2(
!!definition?.properties &&
!definition.properties['manual'] &&
!definition.properties['interval'] &&
!definition.properties['alert']
!definition.properties['alert'] &&
!definition.properties['incident']
) {
setGlobalValidationError('trigger_start', "Workflow Should alteast have one trigger.");
setGlobalValidationError('trigger_start', "Workflow Should at least have one trigger.");
return false;

}
Expand All @@ -38,6 +39,12 @@ export function globalValidatorV2(
return false;
}

const incidentActions = Object.values(definition.properties.incident||{}).filter(Boolean)
if(definition?.properties && definition.properties['incident'] && incidentActions.length==0){
setGlobalValidationError('incident', "Workflow incident trigger cannot be empty.");
return false;
}

const anyStepOrAction = definition?.sequence?.length > 0;
if (!anyStepOrAction) {
setGlobalValidationError(null,
Expand Down
70 changes: 45 additions & 25 deletions keep-ui/app/workflows/builder/editors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Subtitle,
Icon,
Button,
Switch,
Divider,
} from "@tremor/react";
import { KeyIcon } from "@heroicons/react/20/solid";
Expand All @@ -15,6 +16,7 @@ import {
BackspaceIcon,
FunnelIcon,
} from "@heroicons/react/24/outline";
import React from "react";
import useStore, { V2Properties } from "./builder-store";
import { useEffect, useRef, useState } from "react";

Expand Down Expand Up @@ -263,8 +265,6 @@ function WorkflowEditorV2({
selectedNode: string | null;
saveRef: React.MutableRefObject<boolean>;
}) {
const isTrigger = ['interval', 'manual', 'alert'].includes(selectedNode || '')


const updateAlertFilter = (filter: string, value: string) => {
const currentFilters = properties.alert || {};
Expand All @@ -282,7 +282,6 @@ function WorkflowEditorV2({
}
};


const deleteFilter = (filter: string) => {
const currentFilters = { ...properties.alert };
delete currentFilters[filter];
Expand Down Expand Up @@ -310,7 +309,7 @@ function WorkflowEditorV2({
<>
<Title className="mt-2.5">Workflow Settings</Title>
{propertyKeys.map((key, index) => {
const isTrigger = ["manual", "alert", 'interval'].includes(key) ;
const isTrigger = ["manual", "alert", 'interval', 'incident'].includes(key);
renderDivider = isTrigger && key === selectedNode ? !renderDivider : false;
return (
<div key={key}>
Expand Down Expand Up @@ -380,20 +379,41 @@ function WorkflowEditorV2({
)
);

case "incident":
return selectedNode === 'incident' && <>
<Subtitle className="mt-2.5">Incident events</Subtitle>
{Array("created", "updated", "deleted").map((event) =>
<div key={`incident-${event}`} className="flex">
<Switch
id={event}
checked={properties.incident.events?.indexOf(event) > -1}
onChange={() => {
let events = properties.incident.events || [];
if (events.indexOf(event) > -1) {
events = (events as string[]).filter(e => e !== event)
setProperties({ ...properties, [key]: {events: events } })
} else {
events.push(event);
setProperties({ ...properties, [key]: {events: events} })
}
}}
color={"orange"}
/>
<label htmlFor={`incident-${event}`} className="text-sm text-gray-500">
<Text>{event}</Text>
</label>
</div>
)}
</>;
case "interval":
return (
selectedNode === "interval" && (
<TextInput
placeholder={`Set the ${key}`}

onChange={(e: any) =>
handleChange(key, e.target.value)
}
value={properties[key] || "" as string}
/>
)
);
case "disabled":
return selectedNode === "interval" && (<TextInput
placeholder={`Set the ${key}`}
onChange={(e: any) =>
handleChange(key, e.target.value)
}
value={properties[key] || ""as string}
/>);
case "isabled":
return (
<div key={key}>
<input
Expand All @@ -412,13 +432,13 @@ function WorkflowEditorV2({
onChange={(e: any) =>
handleChange(key, e.target.value)
}
value={properties[key] || "" as string}
/>
);
}
})()}
</div>
);
value={properties[key] || ""as string}
/>
);
}
})()}
</div>
);
})}
</>
);
Expand Down Expand Up @@ -489,7 +509,7 @@ export function StepEditorV2({

return (
<EditorLayout>
<Title className="capitalize">{providerType} Editor</Title>
<Title className="capitalize">{providerType}1 Editor</Title>
<Text className="mt-1">Unique Identifier</Text>
<TextInput
className="mb-2.5"
Expand Down
20 changes: 19 additions & 1 deletion keep-ui/app/workflows/builder/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ export function getToolboxConfiguration(providers: Provider[]) {
}
},
},
{
type: "incident",
componentType: "trigger",
name: "Incident",
id: 'incident',
properties: {
incident: {
events: [],
}
},
},
],
},
{
Expand Down Expand Up @@ -298,6 +309,8 @@ export function parseWorkflow(
}, {});
} else if (currType === "manual") {
value = "true";
} else if (currType === "incident") {
value = {events: curr.events};
}
prev[currType] = value;
return prev;
Expand Down Expand Up @@ -508,7 +521,12 @@ export function buildAlert(definition: Definition): Alert {
value: alert.properties.interval,
});
}

if (alert.properties.incident) {
triggers.push({
type: "incident",
events: alert.properties.incident.events,
});
}
return {
id: alertId,
name: name,
Expand Down
2 changes: 1 addition & 1 deletion keep-ui/utils/reactFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ export function getTriggerStep(properties: V2Properties) {
}

Object.keys(properties).forEach((key) => {
if (['interval', 'manual', 'alert'].includes(key) && properties[key]) {
if (['interval', 'manual', 'alert', 'incident'].includes(key) && properties[key]) {
_steps.push({
id: key,
type: key,
Expand Down
23 changes: 17 additions & 6 deletions keep/api/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def create_workflow_execution(
event_id: str = None,
fingerprint: str = None,
execution_id: str = None,
event_type: str = "alert",
) -> str:
with Session(engine) as session:
try:
Expand All @@ -126,13 +127,21 @@ def create_workflow_execution(
# Ensure the object has an id
session.flush()
execution_id = workflow_execution.id
if fingerprint:
if fingerprint and event_type == "alert":
workflow_to_alert_execution = WorkflowToAlertExecution(
workflow_execution_id=execution_id,
alert_fingerprint=fingerprint,
event_id=event_id,
)
session.add(workflow_to_alert_execution)
elif event_type == "incident":
workflow_to_incident_execution = WorkflowToIncidentExecution(
workflow_execution_id=execution_id,
alert_fingerprint=fingerprint,
incident_id=event_id,
)
session.add(workflow_to_incident_execution)

session.commit()
return execution_id
except IntegrityError:
Expand Down Expand Up @@ -687,9 +696,8 @@ def get_workflow_executions(
).scalar()
avgDuration = avgDuration if avgDuration else 0.0

query = (
query.order_by(desc(WorkflowExecution.started)).limit(limit).offset(offset)
)
query = (query.order_by(desc(WorkflowExecution.started)).limit(limit).offset(offset)
)
# Execute the query
workflow_executions = query.all()

Expand Down Expand Up @@ -2366,7 +2374,7 @@ def get_incidents_count(


def get_incident_alerts_by_incident_id(
tenant_id: str, incident_id: str, limit: int, offset: int
tenant_id: str, incident_id: str, limit: Optional[int] = None, offset: Optional[int] = None
) -> (List[Alert], int):
with Session(engine) as session:
query = (
Expand All @@ -2384,7 +2392,10 @@ def get_incident_alerts_by_incident_id(

total_count = query.count()

return query.limit(limit).offset(offset).all(), total_count
if limit and offset:
query = query.limit(limit).offset(offset)

return query.all(), total_count


def get_alerts_data_for_incident(
Expand Down
Loading

0 comments on commit 286e303

Please sign in to comment.