Skip to content

Commit

Permalink
feat: Possibility to run workflows for incidents manually (#2226)
Browse files Browse the repository at this point in the history
  • Loading branch information
VladimirFilonov authored Oct 17, 2024
1 parent 82e83b7 commit de6f06e
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 26 deletions.
4 changes: 2 additions & 2 deletions keep-ui/app/alerts/alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import AlertNoteModal from "./alert-note-modal";
import { useProviders } from "utils/hooks/useProviders";
import { AlertDto } from "./models";
import { AlertMethodModal } from "./alert-method-modal";
import AlertRunWorkflowModal from "./alert-run-workflow-modal";
import ManualRunWorkflowModal from "@/app/workflows/manual-run-workflow-modal";
import AlertDismissModal from "./alert-dismiss-modal";
import { ViewAlertModal } from "./ViewAlertModal";
import { useRouter, useSearchParams } from "next/navigation";
Expand Down Expand Up @@ -151,7 +151,7 @@ export default function Alerts({ presetName }: AlertsProps) {
alert={noteModalAlert ?? null}
/>
{selectedPreset && <AlertMethodModal presetName={selectedPreset.name} />}
<AlertRunWorkflowModal
<ManualRunWorkflowModal
alert={runWorkflowModalAlert}
handleClose={() => setRunWorkflowModalAlert(null)}
/>
Expand Down
28 changes: 27 additions & 1 deletion keep-ui/app/incidents/[id]/incident-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IncidentDto } from "../models";
import CreateOrUpdateIncident from "../create-or-update-incident";
import Modal from "@/components/ui/Modal";
import React, { useState } from "react";
import { MdBlock, MdDone, MdModeEdit } from "react-icons/md";
import {MdBlock, MdDone, MdModeEdit, MdPlayArrow} from "react-icons/md";
import { useIncident, useIncidentFutureIncidents } from "@/utils/hooks/useIncidents";

import {
Expand All @@ -23,6 +23,7 @@ import {STATUS_ICONS} from "@/app/incidents/statuses";
import remarkRehype from "remark-rehype";
import rehypeRaw from "rehype-raw";
import Markdown from "react-markdown";
import ManualRunWorkflowModal from "@/app/workflows/manual-run-workflow-modal";

interface Props {
incident: IncidentDto;
Expand Down Expand Up @@ -92,6 +93,9 @@ export default function IncidentInformation({ incident }: Props) {
const { mutate } = useIncident(incident.id);
const [isFormOpen, setIsFormOpen] = useState<boolean>(false);

const [runWorkflowModalIncident, setRunWorkflowModalIncident] =
useState<IncidentDto | null>();

const handleCloseForm = () => {
setIsFormOpen(false);
};
Expand All @@ -104,6 +108,10 @@ export default function IncidentInformation({ incident }: Props) {
setIsFormOpen(false);
mutate();
};
const handleRunWorkflow = () => {
setRunWorkflowModalIncident(incident);
mutate();
};

const [changeStatusIncident, setChangeStatusIncident] =
useState<IncidentDto | null>();
Expand Down Expand Up @@ -141,12 +149,26 @@ export default function IncidentInformation({ incident }: Props) {
<Title className="flex-grow items-center">
{incident.is_confirmed ? "⚔️ " : "Possible "}Incident
</Title>
<Button
color="orange"
size="xs"
variant="secondary"
icon={MdPlayArrow}
tooltip="Run Workflow"
onClick={(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
handleRunWorkflow();
}}
/>
{incident.is_confirmed && (

<Button
color="orange"
size="xs"
variant="secondary"
icon={MdModeEdit}
tooltip="Edit Incident"
onClick={(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
Expand Down Expand Up @@ -312,6 +334,10 @@ export default function IncidentInformation({ incident }: Props) {
mutate={mutate}
handleClose={() => setChangeStatusIncident(null)}
/>
<ManualRunWorkflowModal
incident={runWorkflowModalIncident}
handleClose={() => setRunWorkflowModalIncident(null)}
/>
</div>
);
}
24 changes: 23 additions & 1 deletion keep-ui/app/incidents/incidents-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import {
MdRemoveCircle,
MdModeEdit,
MdKeyboardDoubleArrowRight,
MdKeyboardDoubleArrowRight, MdPlayArrow,
} from "react-icons/md";
import { useSession } from "next-auth/react";
import { IncidentDto, PaginatedIncidentsDto } from "./models";
Expand All @@ -25,6 +25,8 @@ import {STATUS_ICONS} from "@/app/incidents/statuses";
import Markdown from "react-markdown";
import remarkRehype from "remark-rehype";
import rehypeRaw from "rehype-raw";
import ManualRunWorkflowModal from "@/app/workflows/manual-run-workflow-modal";
import {AlertDto} from "@/app/alerts/models";

const columnHelper = createColumnHelper<IncidentDto>();

Expand Down Expand Up @@ -53,13 +55,21 @@ export default function IncidentsTable({
});
const [changeStatusIncident, setChangeStatusIncident] =
useState<IncidentDto | null>();
const [runWorkflowModalIncident, setRunWorkflowModalIncident] =
useState<IncidentDto | null>();

const handleChangeStatus = (e: React.MouseEvent, incident: IncidentDto) => {
e.preventDefault();
e.stopPropagation();
setChangeStatusIncident(incident);
};

const handleRunWorkflow = (e: React.MouseEvent, incident: IncidentDto) => {
e.preventDefault();
e.stopPropagation();
setRunWorkflowModalIncident(incident);
}

useEffect(() => {
if (incidents.limit != pagination.pageSize) {
setPagination({
Expand Down Expand Up @@ -198,6 +208,14 @@ export default function IncidentsTable({
tooltip="Change status"
onClick={(e) => handleChangeStatus(e, row.original!)}
/>
<Button
color="orange"
size="xs"
variant="secondary"
icon={MdPlayArrow}
tooltip="Run Workflow"
onClick={(e) => handleRunWorkflow(e, row.original!)}
/>
<Button
color="red"
size="xs"
Expand Down Expand Up @@ -248,6 +266,10 @@ export default function IncidentsTable({
mutate={mutate}
handleClose={() => setChangeStatusIncident(null)}
/>
<ManualRunWorkflowModal
incident={runWorkflowModalIncident}
handleClose={() => setRunWorkflowModalIncident(null)}
/>
<div className="mt-4 mb-8">
<IncidentPagination table={table} isRefreshAllowed={true} />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { Button, Select, SelectItem } from "@tremor/react";
import { AlertDto } from "./models";
import {Button, Select, SelectItem, Title} from "@tremor/react";

import Modal from "@/components/ui/Modal";
import { useWorkflows } from "utils/hooks/useWorkflows";
import { useState } from "react";
import { useSession } from "next-auth/react";
import { getApiURL } from "utils/apiUrl";
import { toast } from "react-toastify";
import { useRouter } from "next/navigation";
import { IncidentDto } from "@/app/incidents/models";
import { AlertDto } from "@/app/alerts/models";

interface Props {
alert: AlertDto | null | undefined;
alert?: AlertDto | null | undefined;
incident?: IncidentDto | null | undefined;
handleClose: () => void;
}

export default function AlertRunWorkflowModal({ alert, handleClose }: Props) {
export default function ManualRunWorkflowModal({ alert, incident, handleClose }: Props) {
/**
*
*/
Expand All @@ -25,7 +28,7 @@ export default function AlertRunWorkflowModal({ alert, handleClose }: Props) {
const router = useRouter();
const apiUrl = getApiURL();

const isOpen = !!alert;
const isOpen = !!alert || !!incident;

const clearAndClose = () => {
setSelectedWorkflowId(undefined);
Expand All @@ -41,7 +44,7 @@ export default function AlertRunWorkflowModal({ alert, handleClose }: Props) {
Authorization: `Bearer ${session?.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(alert),
body: JSON.stringify({"type": alert ? "alert" : "incident", "body": alert ? alert : incident}),
}
);

Expand All @@ -61,6 +64,7 @@ export default function AlertRunWorkflowModal({ alert, handleClose }: Props) {

return (
<Modal onClose={clearAndClose} isOpen={isOpen} className="overflow-visible">
<Title className="mb-1">Select Workflow to run</Title>
{workflows && (
<Select
value={selectedWorkflowId}
Expand Down
28 changes: 18 additions & 10 deletions keep/api/routes/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
get_workflow_by_name,
)
from keep.api.core.db import get_workflow_executions as get_workflow_executions_db
from keep.api.models.alert import AlertDto
from keep.api.models.alert import AlertDto, IncidentDto
from keep.api.models.workflow import (
WorkflowCreateOrUpdateDTO,
WorkflowDTO,
Expand Down Expand Up @@ -181,25 +181,33 @@ def run_workflow(

# Finally, run it
try:

if body.get("type", "alert") == "alert":
event_class = AlertDto
else:
event_class = IncidentDto

event_body = body.get("body", {})

# if its event that was triggered by the UI with the Modal
if "test-workflow" in body.get("fingerprint", "") or not body:
if "test-workflow" in event_body.get("fingerprint", "") or not body:
# some random
body["id"] = body.get("fingerprint", "manual-run")
body["name"] = body.get("fingerprint", "manual-run")
body["lastReceived"] = datetime.datetime.now(
event_body["id"] = event_body.get("fingerprint", "manual-run")
event_body["name"] = event_body.get("fingerprint", "manual-run")
event_body["lastReceived"] = datetime.datetime.now(
tz=datetime.timezone.utc
).isoformat()
if "source" in body and not isinstance(body["source"], list):
body["source"] = [body["source"]]
if "source" in event_body and not isinstance(event_body["source"], list):
event_body["source"] = [event_body["source"]]
try:
alert = AlertDto(**body)
event = event_class(**event_body)
except TypeError:
raise HTTPException(
status_code=400,
detail="Invalid alert format",
detail="Invalid event format",
)
workflow_execution_id = workflowmanager.scheduler.handle_manual_event_workflow(
workflow_id, tenant_id, created_by, alert
workflow_id, tenant_id, created_by, event
)
except Exception as e:
logger.exception(
Expand Down
23 changes: 17 additions & 6 deletions keep/workflowmanager/workflowscheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,20 +211,30 @@ def run_workflow_wrapper(
}

def handle_manual_event_workflow(
self, workflow_id, tenant_id, triggered_by_user, alert: AlertDto
self, workflow_id, tenant_id, triggered_by_user, event: [AlertDto | IncidentDto]
):
self.logger.info(f"Running manual event workflow {workflow_id}...")
try:
unique_execution_number = self._get_unique_execution_number()
self.logger.info(f"Unique execution number: {unique_execution_number}")

if isinstance(event, IncidentDto):
event_id = str(event.id)
event_type = "incident"
fingerprint = "incident:{}".format(event_id)
else:
event_id = event.event_id
event_type = "alert"
fingerprint = event.fingerprint

workflow_execution_id = create_workflow_execution(
workflow_id=workflow_id,
tenant_id=tenant_id,
triggered_by=f"manually by {triggered_by_user}",
execution_number=unique_execution_number,
fingerprint=alert.fingerprint,
event_id=alert.event_id,
event_type="alert",
fingerprint=fingerprint,
event_id=event_id,
event_type=event_type,
)
self.logger.info(f"Workflow execution id: {workflow_execution_id}")
# This is kinda WTF exception since create_workflow_execution shouldn't fail for manual
Expand All @@ -242,15 +252,16 @@ def handle_manual_event_workflow(
},
)
with self.lock:
alert.trigger = "manual"
event.trigger = "manual"
self.workflows_to_run.append(
{
"workflow_id": workflow_id,
"workflow_execution_id": workflow_execution_id,
"tenant_id": tenant_id,
"triggered_by": "manual",
"triggered_by_user": triggered_by_user,
"event": alert,
"event": event,
"retry": True,
}
)
return workflow_execution_id
Expand Down

0 comments on commit de6f06e

Please sign in to comment.