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 (
@@ -79,9 +66,9 @@ export default function WorkflowMenu({
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