From 1f4eba8daeda36db7c10feebe2da37c7fe5eb7ff Mon Sep 17 00:00:00 2001
From: Rajesh Jonnalagadda <38752904+rajeshj11@users.noreply.github.com>
Date: Wed, 11 Sep 2024 19:27:49 +0530
Subject: [PATCH] fix: use custom worflowRun hook to run the workflow at all
places (#1890)
Signed-off-by: Rajesh Jonnalagadda <38752904+rajeshj11@users.noreply.github.com>
---
.../workflows/[workflow_id]/executions.tsx | 47 ++--
keep-ui/app/workflows/mockworkflows.tsx | 4 +-
keep-ui/app/workflows/workflow-menu.tsx | 25 +-
keep-ui/app/workflows/workflow-tile.tsx | 243 +++---------------
keep-ui/utils/hooks/useWorkflowRun.ts | 190 ++++++++++----
keep/api/models/workflow.py | 2 +-
keep/api/routes/workflows.py | 94 +++----
keep/api/utils/pagination.py | 3 +-
keep/workflowmanager/workflowstore.py | 64 +++++
9 files changed, 313 insertions(+), 359 deletions(-)
diff --git a/keep-ui/app/workflows/[workflow_id]/executions.tsx b/keep-ui/app/workflows/[workflow_id]/executions.tsx
index 9d134d50a9..d2b844251f 100644
--- a/keep-ui/app/workflows/[workflow_id]/executions.tsx
+++ b/keep-ui/app/workflows/[workflow_id]/executions.tsx
@@ -27,6 +27,7 @@ import { useWorkflowRun } from "utils/hooks/useWorkflowRun";
import BuilderWorkflowTestRunModalContent from "../builder/builder-workflow-testrun-modal";
import Modal from "react-modal";
import { TableFilters } from "./table-filters";
+import AlertTriggerModal from "../workflow-run-with-alert-modal";
const tabs = [
{ name: "All Time", value: 'alltime' },
@@ -67,10 +68,10 @@ export const FilterTabs = ({
export function StatsCard({ children, data }: { children: any, data?: string }) {
return
- {!!data &&
- {data}
-
}
- {children}
+ {!!data &&
+ {data}
+
}
+ {children}
}
@@ -109,11 +110,12 @@ export default function WorkflowDetailPage({
} = useWorkflowExecutionsV2(params.workflow_id, tab, executionPagination.limit, executionPagination.offset);
const {
- loading,
- runModalOpen,
- setRunModalOpen,
- runningWorkflowExecution,
- setRunningWorkflowExecution } = useWorkflowRun(data?.workflow?.workflow_raw!)
+ isRunning,
+ handleRunClick,
+ getTriggerModalProps,
+ isRunButtonDisabled,
+ message,
+ } = useWorkflowRun(data?.workflow!)
if (isLoading || !data) return ;
@@ -144,7 +146,7 @@ export default function WorkflowDetailPage({
} else if (num >= 1_000) {
return `${(num / 1_000).toFixed(1)}k`;
} else {
- return num.toString();
+ return num?.toString() ?? "";
}
};
@@ -159,7 +161,12 @@ export default function WorkflowDetailPage({
{/*TO DO update searchParams for these filters*/}
-
+ {!!data.workflow && }
{data?.items && (
@@ -170,16 +177,14 @@ export default function WorkflowDetailPage({
{formatNumber(data.count ?? 0)}
- {/*
__ from last month
*/}
-
+
Pass / Fail ratio
{formatNumber(data.passCount)}{'/'}{formatNumber(data.failCount)}
- {/*
__ from last month
*/}
@@ -189,7 +194,6 @@ export default function WorkflowDetailPage({
{(data.count ? (data.passCount / data.count) * 100 : 0).toFixed(2)}{"%"}
- {/*
__ from last month
*/}
@@ -199,7 +203,6 @@ export default function WorkflowDetailPage({
{(data.avgDuration ?? 0).toFixed(2)}
- {/*
__ from last month
*/}
@@ -221,16 +224,8 @@ export default function WorkflowDetailPage({
)}
- { setRunModalOpen(false); setRunningWorkflowExecution(null) }}
- className="bg-gray-50 p-4 md:p-10 mx-auto max-w-7xl mt-20 border border-orange-600/50 rounded-md"
- >
- { setRunModalOpen(false); setRunningWorkflowExecution(null) }}
- workflowExecution={runningWorkflowExecution}
- />
-
+ {!!data.workflow && !!getTriggerModalProps && }
>
);
}
diff --git a/keep-ui/app/workflows/mockworkflows.tsx b/keep-ui/app/workflows/mockworkflows.tsx
index 2ca2a81cab..0ea7379312 100644
--- a/keep-ui/app/workflows/mockworkflows.tsx
+++ b/keep-ui/app/workflows/mockworkflows.tsx
@@ -24,7 +24,7 @@ export function WorkflowSteps({ workflow }: { workflow: MockWorkflow }) {
return (
<>
{provider && (
-
+
{index > 0 && (
)}
@@ -48,7 +48,7 @@ export function WorkflowSteps({ workflow }: { workflow: MockWorkflow }) {
return (
<>
{provider && (
-
+
{(index > 0 || isStepPresent) && (
)}
diff --git a/keep-ui/app/workflows/workflow-menu.tsx b/keep-ui/app/workflows/workflow-menu.tsx
index 732ff3d68c..d13e85aa79 100644
--- a/keep-ui/app/workflows/workflow-menu.tsx
+++ b/keep-ui/app/workflows/workflow-menu.tsx
@@ -11,10 +11,8 @@ interface WorkflowMenuProps {
onView?: () => void;
onDownload?: () => void;
onBuilder?: () => void;
- allProvidersInstalled: boolean;
- hasManualTrigger: boolean;
- hasAlertTrigger: boolean;
- isWorkflowDisabled:boolean
+ isRunButtonDisabled: boolean;
+ runButtonToolTip?: string;
}
@@ -24,24 +22,13 @@ export default function WorkflowMenu({
onView,
onDownload,
onBuilder,
- allProvidersInstalled,
- hasManualTrigger,
- hasAlertTrigger,
- isWorkflowDisabled,
+ isRunButtonDisabled,
+ runButtonToolTip,
}: WorkflowMenuProps) {
- const getDisabledTooltip = () => {
- if (!allProvidersInstalled) return "Not all providers are installed.";
- if (!hasManualTrigger) return "No manual trigger available.";
- if (isWorkflowDisabled) return "Workflow is disabled";
- return "";
- };
const stopPropagation = (e: React.MouseEvent
) => {
e.stopPropagation();
};
- const isRunButtonDisabled = !allProvidersInstalled || (!hasManualTrigger && !hasAlertTrigger) || isWorkflowDisabled;
-
-
return (
diff --git a/keep-ui/app/workflows/workflow-tile.tsx b/keep-ui/app/workflows/workflow-tile.tsx
index 28307ff5da..c462454d1d 100644
--- a/keep-ui/app/workflows/workflow-tile.tsx
+++ b/keep-ui/app/workflows/workflow-tile.tsx
@@ -40,6 +40,7 @@ import {
MdOutlineKeyboardArrowLeft,
} from "react-icons/md";
import { HiBellAlert } from "react-icons/hi2";
+import { useWorkflowRun } from "utils/hooks/useWorkflowRun";
function WorkflowMenuSection({
onDelete,
@@ -47,28 +48,18 @@ function WorkflowMenuSection({
onDownload,
onView,
onBuilder,
- workflow,
+ isRunButtonDisabled,
+ runButtonToolTip,
}: {
onDelete: () => Promise;
onRun: () => Promise;
onDownload: () => void;
onView: () => void;
onBuilder: () => void;
- workflow: Workflow;
+ isRunButtonDisabled: boolean;
+ runButtonToolTip?: string;
}) {
// Determine if all providers are installed
- const allProvidersInstalled = workflow.providers.every(
- (provider) => provider.installed
- );
-
- // Check if there is a manual trigger
- const hasManualTrigger = workflow.triggers.some(
- (trigger) => trigger.type === "manual"
- ); // Replace 'manual' with the actual value that represents a manual trigger in your data
-
- const hasAlertTrigger = workflow.triggers.some(
- (trigger) => trigger.type === "alert"
- );
return (
);
}
@@ -282,11 +271,7 @@ function WorkflowTile({ workflow }: { workflow: Workflow }) {
);
const [formValues, setFormValues] = useState<{ [key: string]: string }>({});
const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({});
- const [isRunning, setIsRunning] = useState(false);
- const [isAlertTriggerModalOpen, setIsAlertTriggerModalOpen] = useState(false);
- const [alertFilters, setAlertFilters] = useState([]);
- const [alertDependencies, setAlertDependencies] = useState([]);
const [openTriggerModal, setOpenTriggerModal] = useState(false);
const alertSource = workflow?.triggers
?.find((w) => w.type === "alert")
@@ -294,6 +279,13 @@ function WorkflowTile({ workflow }: { workflow: Workflow }) {
const [fallBackIcon, setFallBackIcon] = useState(false);
const { providers } = useFetchProviders();
+ const {
+ isRunning,
+ handleRunClick,
+ getTriggerModalProps,
+ isRunButtonDisabled,
+ message,
+ } = useWorkflowRun(workflow!);
const handleConnectProvider = (provider: FullProvider) => {
setSelectedProvider(provider);
@@ -317,89 +309,6 @@ function WorkflowTile({ workflow }: { workflow: Workflow }) {
setFormErrors(updatedFormErrors);
};
- // todo: this logic should move to the backend
- function extractAlertDependencies(workflowRaw: string): string[] {
- const dependencyRegex = /(?((acc, dep) => {
- // Ensure 'dep' is treated as a string
- const match = dep.match(/alert\.([\w.]+)/);
- if (match) {
- acc.push(match[1]);
- }
- return acc;
- }, []);
-
- return uniqueDependencies;
- }
-
- const runWorkflow = async (payload: object) => {
- try {
- setIsRunning(true);
- const response = await fetch(`${apiUrl}/workflows/${workflow.id}/run`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${session?.accessToken}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify(payload),
- });
-
- if (response.ok) {
- // Workflow started successfully
- const responseData = await response.json();
- const { workflow_execution_id } = responseData;
- setIsRunning(false);
- router.push(`/workflows/${workflow.id}/runs/${workflow_execution_id}`);
- } else {
- console.error("Failed to start workflow");
- }
- } catch (error) {
- console.error("An error occurred while starting workflow", error);
- }
- setIsRunning(false);
- };
-
- const handleRunClick = async () => {
- const hasAlertTrigger = workflow.triggers.some(
- (trigger) => trigger.type === "alert"
- );
-
- // if it needs alert payload, than open the modal
- if (hasAlertTrigger) {
- // extract the filters
- // TODO: support more than one trigger
- for (const trigger of workflow.triggers) {
- // at least one trigger is alert, o/w hasAlertTrigger was false
- if (trigger.type === "alert") {
- const staticAlertFilters = trigger.filters || [];
- setAlertFilters(staticAlertFilters);
- break;
- }
- }
- const dependencies = extractAlertDependencies(workflow.workflow_raw);
- setAlertDependencies(dependencies);
- setIsAlertTriggerModalOpen(true);
- return;
- }
- // else, manual trigger, just run it
- else {
- runWorkflow({});
- }
- };
-
- const handleAlertTriggerModalSubmit = (payload: any) => {
- runWorkflow(payload); // Function to run the workflow with the payload
- };
-
const handleDeleteClick = async () => {
try {
const response = await fetch(`${apiUrl}/workflows/${workflow.id}`, {
@@ -649,13 +558,14 @@ function WorkflowTile({ workflow }: { workflow: Workflow }) {
}}
>
- {WorkflowMenuSection({
+ {!!handleRunClick && WorkflowMenuSection({
onDelete: handleDeleteClick,
onRun: handleRunClick,
onDownload: handleDownloadClick,
onView: handleViewClick,
onBuilder: handleBuilderClick,
- workflow,
+ runButtonToolTip: message,
+ isRunButtonDisabled: !!isRunButtonDisabled,
})}
@@ -765,13 +675,9 @@ function WorkflowTile({ workflow }: { workflow: Workflow }) {
- setIsAlertTriggerModalOpen(false)}
- onSubmit={handleAlertTriggerModalSubmit}
- staticFields={alertFilters}
- dependencies={alertDependencies}
- />
+ {!!getTriggerModalProps && }
{
@@ -813,13 +719,15 @@ export function WorkflowTileOld({ workflow }: { workflow: Workflow }) {
);
const [formValues, setFormValues] = useState<{ [key: string]: string }>({});
const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({});
- const [isRunning, setIsRunning] = useState(false);
- const [isAlertTriggerModalOpen, setIsAlertTriggerModalOpen] = useState(false);
-
- const [alertFilters, setAlertFilters] = useState([]);
- const [alertDependencies, setAlertDependencies] = useState([]);
const { providers } = useFetchProviders();
+ const {
+ isRunning,
+ handleRunClick,
+ isRunButtonDisabled,
+ message,
+ getTriggerModalProps,
+ } = useWorkflowRun(workflow!);
const handleConnectProvider = (provider: FullProvider) => {
setSelectedProvider(provider);
@@ -843,88 +751,6 @@ export function WorkflowTileOld({ workflow }: { workflow: Workflow }) {
setFormErrors(updatedFormErrors);
};
- // todo: this logic should move to the backend
- function extractAlertDependencies(workflowRaw: string): string[] {
- const dependencyRegex = /(?((acc, dep) => {
- // Ensure 'dep' is treated as a string
- const match = dep.match(/alert\.([\w.]+)/);
- if (match) {
- acc.push(match[1]);
- }
- return acc;
- }, []);
-
- return uniqueDependencies;
- }
-
- const runWorkflow = async (payload: object) => {
- try {
- setIsRunning(true);
- const response = await fetch(`${apiUrl}/workflows/${workflow.id}/run`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${session?.accessToken}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify(payload),
- });
-
- if (response.ok) {
- // Workflow started successfully
- const responseData = await response.json();
- const { workflow_execution_id } = responseData;
- setIsRunning(false);
- router.push(`/workflows/${workflow.id}/runs/${workflow_execution_id}`);
- } else {
- console.error("Failed to start workflow");
- }
- } catch (error) {
- console.error("An error occurred while starting workflow", error);
- }
- setIsRunning(false);
- };
-
- const handleRunClick = async () => {
- const hasAlertTrigger = workflow.triggers.some(
- (trigger) => trigger.type === "alert"
- );
-
- // if it needs alert payload, than open the modal
- if (hasAlertTrigger) {
- // extract the filters
- // TODO: support more than one trigger
- for (const trigger of workflow.triggers) {
- // at least one trigger is alert, o/w hasAlertTrigger was false
- if (trigger.type === "alert") {
- const staticAlertFilters = trigger.filters || [];
- setAlertFilters(staticAlertFilters);
- break;
- }
- }
- const dependencies = extractAlertDependencies(workflow.workflow_raw);
- setAlertDependencies(dependencies);
- setIsAlertTriggerModalOpen(true);
- return;
- }
- // else, manual trigger, just run it
- else {
- runWorkflow({});
- }
- };
-
- const handleAlertTriggerModalSubmit = (payload: any) => {
- runWorkflow(payload); // Function to run the workflow with the payload
- };
const handleDeleteClick = async () => {
try {
@@ -1028,13 +854,14 @@ export function WorkflowTileOld({ workflow }: { workflow: Workflow }) {
{workflow.name}
- {WorkflowMenuSection({
+ {!!handleRunClick && WorkflowMenuSection({
onDelete: handleDeleteClick,
onRun: handleRunClick,
onDownload: handleDownloadClick,
onView: handleViewClick,
onBuilder: handleBuilderClick,
- workflow,
+ runButtonToolTip: message,
+ isRunButtonDisabled: !!isRunButtonDisabled,
})}
@@ -1186,13 +1013,9 @@ export function WorkflowTileOld({ workflow }: { workflow: Workflow }) {
)}
-
setIsAlertTriggerModalOpen(false)}
- onSubmit={handleAlertTriggerModalSubmit}
- staticFields={alertFilters}
- dependencies={alertDependencies}
- />
+ {!!getTriggerModalProps && }
);
}
diff --git a/keep-ui/utils/hooks/useWorkflowRun.ts b/keep-ui/utils/hooks/useWorkflowRun.ts
index 4edc9f4383..a35ca86bca 100644
--- a/keep-ui/utils/hooks/useWorkflowRun.ts
+++ b/keep-ui/utils/hooks/useWorkflowRun.ts
@@ -1,59 +1,159 @@
-import { WorkflowExecutionFailure, WorkflowExecution } from "app/workflows/builder/types";
+import { useState } from "react";
import { useSession } from "next-auth/react";
-import { useEffect, useState } from "react";
import { getApiURL } from "utils/apiUrl";
+import { useRouter } from "next/navigation";
+import { Filter, Workflow } from "app/workflows/models";
-export const useWorkflowRun = (workflowRaw: string) => {
- const [runningWorkflowExecution, setRunningWorkflowExecution] = useState<
- WorkflowExecution | WorkflowExecutionFailure | null
- >(null);
- const [runModalOpen, setRunModalOpen] = useState(false);
- const [loading, setLoading] = useState(true);
+export const useWorkflowRun = (workflow: Workflow) => {
+
+ const router = useRouter();
+ const [isRunning, setIsRunning] = useState(false);
const { data: session, status, update } = useSession();
const accessToken = session?.accessToken;
+ const [isAlertTriggerModalOpen, setIsAlertTriggerModalOpen] = useState(false);
+ let message = ""
+ const [alertFilters, setAlertFilters] = useState
([]);
+ const [alertDependencies, setAlertDependencies] = useState([]);
const apiUrl = getApiURL();
- const url = `${apiUrl}/workflows/test`;
- const method = "POST";
- const headers = {
- "Content-Type": "text/html",
- Authorization: `Bearer ${accessToken}`,
+
+ if (!workflow) {
+ return {};
+ }
+ const allProvidersInstalled = workflow?.providers?.every(
+ (provider) => provider.installed
+ );
+
+ // Check if there is a manual trigger
+ const hasManualTrigger = workflow?.triggers?.some(
+ (trigger) => trigger.type === "manual"
+ ); // Replace 'manual' with the actual value that represents a manual trigger in your data
+
+ const hasAlertTrigger = workflow?.triggers?.some(
+ (trigger) => trigger.type === "alert"
+ );
+
+ const isWorkflowDisabled = !!workflow?.disabled
+
+ const getDisabledTooltip = () => {
+ if (!allProvidersInstalled) return "Not all providers are installed.";
+ if (!hasManualTrigger) return "No manual trigger available.";
+ if(isWorkflowDisabled) {
+ return "Workflow is Disabled";
+ }
+ return message;
+ };
+
+ const isRunButtonDisabled = isWorkflowDisabled || !allProvidersInstalled || (!hasManualTrigger && !hasAlertTrigger);
+
+ if (isRunButtonDisabled) {
+ message = getDisabledTooltip();
+ }
+ function extractAlertDependencies(workflowRaw: string): string[] {
+ const dependencyRegex = /(?((acc, dep) => {
+ // Ensure 'dep' is treated as a string
+ const match = dep.match(/alert\.([\w.]+)/);
+ if (match) {
+ acc.push(match[1]);
+ }
+ return acc;
+ }, []);
+
+ return uniqueDependencies;
+ }
+
+ const runWorkflow = async (payload: object) => {
+ try {
+ if (!workflow) {
+ return;
+ }
+ setIsRunning(true);
+ const response = await fetch(`${apiUrl}/workflows/${workflow?.id}/run`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(payload),
+ });
+
+ if (response.ok) {
+ // Workflow started successfully
+ const responseData = await response.json();
+ const { workflow_execution_id } = responseData;
+ router.push(`/workflows/${workflow?.id}/runs/${workflow_execution_id}`);
+ } else {
+ console.error("Failed to start workflow");
+ }
+ } catch (error) {
+ console.error("An error occurred while starting workflow", error);
+ } finally {
+ setIsRunning(false);
+ }
+ setIsRunning(false);
};
- useEffect(() => {
- if (runModalOpen) {
- const body = workflowRaw;
- setLoading(true);
- fetch(url, { method, headers, body })
- .then((response) => {
- if (response.ok) {
- response.json().then((data) => {
- setRunningWorkflowExecution({
- ...data,
- });
- });
- } else {
- response.json().then((data) => {
- setRunningWorkflowExecution({
- error: data?.detail ?? "Unknown error",
- });
- });
- }
- })
- .catch((error) => {
- alert(`Error: ${error}`);
- setRunModalOpen(false);
- }).finally(()=>{
- setLoading(false);
- })
+ const handleRunClick = async () => {
+ if (!workflow) {
+ return;
+ }
+ const hasAlertTrigger = workflow?.triggers?.some(
+ (trigger) => trigger.type === "alert"
+ );
+
+ // if it needs alert payload, than open the modal
+ if (hasAlertTrigger) {
+ // extract the filters
+ // TODO: support more than one trigger
+ for (const trigger of workflow?.triggers) {
+ // at least one trigger is alert, o/w hasAlertTrigger was false
+ if (trigger.type === "alert") {
+ const staticAlertFilters = trigger.filters || [];
+ setAlertFilters(staticAlertFilters);
+ break;
+ }
+ }
+ const dependencies = extractAlertDependencies(workflow?.workflow_raw);
+ setAlertDependencies(dependencies);
+ setIsAlertTriggerModalOpen(true);
+ return;
+ }
+ // else, manual trigger, just run it
+ else {
+ runWorkflow({});
}
- }, [workflowRaw, runModalOpen])
+ };
+
+ const handleAlertTriggerModalSubmit = (payload: any) => {
+ runWorkflow(payload); // Function to run the workflow with the payload
+ };
+
+
+ const getTriggerModalProps = () => {
+ return {
+ isOpen: isAlertTriggerModalOpen,
+ onClose: () => setIsAlertTriggerModalOpen(false),
+ onSubmit: handleAlertTriggerModalSubmit,
+ staticFields: alertFilters,
+ dependencies: alertDependencies
+ }
+ }
return {
- loading,
- runModalOpen,
- setRunModalOpen,
- runningWorkflowExecution,
- setRunningWorkflowExecution,
+ handleRunClick,
+ isRunning,
+ getTriggerModalProps,
+ isRunButtonDisabled,
+ message
}
};
diff --git a/keep/api/models/workflow.py b/keep/api/models/workflow.py
index 8c6b753142..26d28f2e1b 100644
--- a/keep/api/models/workflow.py
+++ b/keep/api/models/workflow.py
@@ -29,7 +29,7 @@ class WorkflowDTO(BaseModel):
creation_time: datetime
triggers: List[dict] = None
interval: int
- disabled:bool
+ disabled: bool = False
last_execution_time: datetime = None
last_execution_status: str = None
providers: List[ProviderDTO]
diff --git a/keep/api/routes/workflows.py b/keep/api/routes/workflows.py
index 3838b12b7a..47d44f6ae6 100644
--- a/keep/api/routes/workflows.py
+++ b/keep/api/routes/workflows.py
@@ -30,7 +30,6 @@
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.workflow import (
- ProviderDTO,
WorkflowCreateOrUpdateDTO,
WorkflowDTO,
WorkflowExecutionDTO,
@@ -41,7 +40,6 @@
from keep.identitymanager.authenticatedentity import AuthenticatedEntity
from keep.identitymanager.identitymanagerfactory import IdentityManagerFactory
from keep.parser.parser import Parser
-from keep.providers.providers_factory import ProvidersFactory
from keep.workflowmanager.workflowmanager import WorkflowManager
from keep.workflowmanager.workflowstore import WorkflowStore
@@ -72,7 +70,6 @@ def get_workflows(
) -> list[WorkflowDTO] | list[dict]:
tenant_id = authenticated_entity.tenant_id
workflowstore = WorkflowStore()
- parser = Parser()
workflows_dto = []
installed_providers = get_installed_providers(tenant_id)
installed_providers_by_type = {}
@@ -109,57 +106,12 @@ def get_workflows(
last_execution_started = None
try:
- workflow_yaml = yaml.safe_load(workflow.workflow_raw)
- providers = parser.get_providers_from_workflow(workflow_yaml)
- except Exception:
- logger.exception("Failed to parse workflow", extra={"workflow": workflow})
- continue
- providers_dto = []
- # get the provider details
- for provider in providers:
- try:
- provider = installed_providers_by_type[provider.get("type")][
- provider.get("name")
- ]
- provider_dto = ProviderDTO(
- name=provider.name,
- type=provider.type,
- id=provider.id,
- installed=True,
- )
- providers_dto.append(provider_dto)
- except KeyError:
- # the provider is not installed, now we want to check:
- # 1. if the provider requires any config - so its not instaleld
- # 2. if the provider does not require any config - consider it as installed
- try:
- conf = ProvidersFactory.get_provider_required_config(
- provider.get("type")
- )
- except ModuleNotFoundError:
- logger.warning(
- "Someone tried to use a non-existing provider in a workflow",
- extra={"provider": provider.get("type")},
- )
- conf = None
- if conf:
- provider_dto = ProviderDTO(
- name=provider.get("name"),
- type=provider.get("type"),
- id=None,
- installed=False,
- )
- # if the provider does not require any config, consider it as installed
- else:
- provider_dto = ProviderDTO(
- name=provider.get("name"),
- type=provider.get("type"),
- id=None,
- installed=True,
- )
- providers_dto.append(provider_dto)
-
- triggers = parser.get_triggers_from_workflow(workflow_yaml)
+ providers_dto, triggers = workflowstore.get_workflow_meta_data(
+ tenant_id=tenant_id, workflow=workflow, installed_providers_by_type=installed_providers_by_type)
+ except Exception as e:
+ logger.error(f"Error fetching workflow meta data: {e}")
+ providers_dto, triggers = [], [] # Default in case of failure
+
# create the workflow DTO
workflow_dto = WorkflowDTO(
id=workflow.id,
@@ -549,6 +501,17 @@ def get_workflow_by_id(
) -> WorkflowExecutionsPaginatedResultsDto:
tenant_id = authenticated_entity.tenant_id
workflow = get_workflow(tenant_id=tenant_id, workflow_id=workflow_id)
+ installed_providers = get_installed_providers(tenant_id)
+ installed_providers_by_type = {}
+ for installed_provider in installed_providers:
+ if installed_provider.type not in installed_providers_by_type:
+ installed_providers_by_type[installed_provider.type] = {
+ installed_provider.name: installed_provider
+ }
+ else:
+ installed_providers_by_type[installed_provider.type][
+ installed_provider.name
+ ] = installed_provider
with tracer.start_as_current_span("get_workflow_executions"):
total_count, workflow_executions, pass_count, fail_count, avgDuration = (
@@ -577,6 +540,27 @@ def get_workflow_by_id(
}
workflow_executions_dtos.append(workflow_execution_dto)
+ workflowstore = WorkflowStore()
+ try:
+ providers_dto, triggers = workflowstore.get_workflow_meta_data(
+ tenant_id=tenant_id, workflow=workflow, installed_providers_by_type=installed_providers_by_type)
+ except Exception as e:
+ logger.error(f"Error fetching workflow meta data: {e}")
+ providers_dto, triggers = [], [] # Default in case of failure
+
+ final_workflow = WorkflowDTO(
+ id=workflow.id,
+ name=workflow.name,
+ description=workflow.description or "[This workflow has no description]",
+ created_by=workflow.created_by,
+ creation_time=workflow.creation_time,
+ interval=workflow.interval,
+ providers=providers_dto,
+ triggers=triggers,
+ workflow_raw=workflow.workflow_raw,
+ last_updated=workflow.last_updated,
+ disabled=workflow.is_disabled,
+ )
return WorkflowExecutionsPaginatedResultsDto(
limit=limit,
offset=offset,
@@ -585,7 +569,7 @@ def get_workflow_by_id(
passCount=pass_count,
failCount=fail_count,
avgDuration=avgDuration,
- workflow=workflow,
+ workflow=final_workflow
)
diff --git a/keep/api/utils/pagination.py b/keep/api/utils/pagination.py
index 2bdc5207f8..fb633a5eb3 100644
--- a/keep/api/utils/pagination.py
+++ b/keep/api/utils/pagination.py
@@ -5,6 +5,7 @@
from keep.api.models.alert import IncidentDto, AlertDto
from keep.api.models.workflow import (
WorkflowExecutionDTO,
+ WorkflowDTO
)
from keep.api.models.db.workflow import * # pylint: disable=unused-wildcard-import
from typing import Optional
@@ -28,5 +29,5 @@ class WorkflowExecutionsPaginatedResultsDto(PaginatedResultsDto):
items: list[WorkflowExecutionDTO]
passCount: int = 0
avgDuration: float = 0.0
- workflow: Optional[Workflow] = None
+ workflow: Optional[WorkflowDTO] = None
failCount: int = 0
diff --git a/keep/workflowmanager/workflowstore.py b/keep/workflowmanager/workflowstore.py
index 7eda1943c5..cfeb7021da 100644
--- a/keep/workflowmanager/workflowstore.py
+++ b/keep/workflowmanager/workflowstore.py
@@ -22,6 +22,10 @@
from keep.api.models.db.workflow import Workflow as WorkflowModel
from keep.parser.parser import Parser
from keep.workflowmanager.workflow import Workflow
+from keep.providers.providers_factory import ProvidersFactory
+from keep.api.models.workflow import (
+ ProviderDTO,
+)
class WorkflowStore:
@@ -331,3 +335,63 @@ def group_last_workflow_executions(self, workflows: list[dict]) -> list[dict]:
]
return results
+
+ def get_workflow_meta_data(self, tenant_id: str, workflow: dict, installed_providers_by_type: dict):
+ providers_dto = []
+ triggers = []
+
+ # Early return if workflow is None
+ if workflow is None:
+ return providers_dto, triggers
+
+ # Step 1: Load workflow YAML and handle potential parsing errors more thoroughly
+ try:
+ workflow_raw_data = workflow.workflow_raw
+ if not isinstance(workflow_raw_data, str):
+ self.logger.error(f"workflow_raw is not a string workflow: {workflow}")
+ return providers_dto, triggers
+
+ # Parse the workflow YAML safely
+ workflow_yaml = yaml.safe_load(workflow_raw_data)
+ if not workflow_yaml:
+ self.logger.error(f"Parsed workflow_yaml is empty or invalid: {workflow_raw_data}")
+ return providers_dto, triggers
+
+ providers = self.parser.get_providers_from_workflow(workflow_yaml)
+ except Exception as e:
+ # Improved logging to capture more details about the error
+ self.logger.error(f"Failed to parse workflow in get_workflow_meta_data: {e}, workflow: {workflow}")
+ return providers_dto, triggers # Return empty providers and triggers in case of error
+
+ # Step 2: Process providers and add them to DTO
+ for provider in providers:
+ try:
+ provider_data = installed_providers_by_type[provider.get("type")][provider.get("name")]
+ provider_dto = ProviderDTO(
+ name=provider_data.name,
+ type=provider_data.type,
+ id=provider_data.id,
+ installed=True,
+ )
+ providers_dto.append(provider_dto)
+ except KeyError:
+ # Handle case where the provider is not installed
+ try:
+ conf = ProvidersFactory.get_provider_required_config(provider.get("type"))
+ except ModuleNotFoundError:
+ self.logger.warning(f"Non-existing provider in workflow: {provider.get('type')}")
+ conf = None
+
+ # Handle providers based on whether they require config
+ provider_dto = ProviderDTO(
+ name=provider.get("name"),
+ type=provider.get("type"),
+ id=None,
+ installed=(conf is None), # Consider it installed if no config is required
+ )
+ providers_dto.append(provider_dto)
+
+ # Step 3: Extract triggers from workflow
+ triggers = self.parser.get_triggers_from_workflow(workflow_yaml)
+
+ return providers_dto, triggers
\ No newline at end of file