Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adding incident workflow triggers #1861

Merged
merged 18 commits into from
Sep 19, 2024
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
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
Loading