diff --git a/frontend/src/components/executions/nav.tsx b/frontend/src/components/executions/nav.tsx index d07bcb4e5..f81d2c616 100644 --- a/frontend/src/components/executions/nav.tsx +++ b/frontend/src/components/executions/nav.tsx @@ -96,11 +96,12 @@ export function WorkflowExecutionNav({ {workflowExecutions.map((execution, index) => ( <HoverCard openDelay={10} closeDelay={10} key={index}> <Link - href={`${baseUrl}/executions/${execution.id.split(":")[1]}`} + href={`${baseUrl}/executions/${parseExecutionId(execution.id)[1]}`} className={cn( buttonVariants({ variant: "default", size: "sm" }), "justify-start bg-background text-muted-foreground shadow-none hover:cursor-default hover:bg-gray-100", - execution.id.split(":")[1] === executionId && "bg-gray-200" + parseExecutionId(execution.id)[1] === executionId && + "bg-gray-200" )} > <div className="flex items-center"> @@ -152,7 +153,7 @@ export function WorkflowExecutionNav({ <Label className="text-xs text-muted-foreground"> Execution ID </Label> - <span>{execution.id.split(":")[1]}</span> + <span>{parseExecutionId(execution.id)[1]}</span> </div> <div className="flex flex-col"> <Label className="text-xs text-muted-foreground"> @@ -238,3 +239,23 @@ export function getExecutionStatusIcon( throw new Error("Invalid status") } } + +/** + * Get the execution ID from a full execution ID + * @param fullExecutionId + * @returns the execution ID + * + * Example: + * - "wf-123:1234567890" -> ["wf-123", "1234567890"] + * - "wf-123:1234567890:1" -> ["wf-123", "1234567890:1"] + */ +function parseExecutionId(fullExecutionId: string): [string, string] { + // Split at most once from the left, keeping any remaining colons in the second part + const splitIndex = fullExecutionId.indexOf(":") + if (splitIndex === -1) { + throw new Error("Invalid execution ID format - missing colon separator") + } + const workflowId = fullExecutionId.slice(0, splitIndex) + const executionId = fullExecutionId.slice(splitIndex + 1) + return [workflowId, executionId] +} diff --git a/tracecat/workflow/executions/dependencies.py b/tracecat/workflow/executions/dependencies.py new file mode 100644 index 000000000..c06f15d6c --- /dev/null +++ b/tracecat/workflow/executions/dependencies.py @@ -0,0 +1,14 @@ +import urllib.parse +from typing import Annotated + +from fastapi import Depends + +from tracecat.workflow.executions.models import ExecutionOrScheduleID + + +def unquote_dep(execution_id: ExecutionOrScheduleID) -> ExecutionOrScheduleID: + return urllib.parse.unquote(execution_id) + + +UnquotedExecutionOrScheduleID = Annotated[ExecutionOrScheduleID, Depends(unquote_dep)] +"""Dependency for an unquoted execution or schedule ID.""" diff --git a/tracecat/workflow/executions/models.py b/tracecat/workflow/executions/models.py index 509d1343f..c73d6ffc0 100644 --- a/tracecat/workflow/executions/models.py +++ b/tracecat/workflow/executions/models.py @@ -20,7 +20,7 @@ RunActionInput, TriggerInputs, ) -from tracecat.identifiers import WorkflowExecutionID, WorkflowID +from tracecat.identifiers import WorkflowExecutionID, WorkflowID, WorkflowScheduleID from tracecat.types.auth import Role from tracecat.workflow.management.models import GetWorkflowDefinitionActivityInputs @@ -35,6 +35,8 @@ ] """Mapped literal types for workflow execution statuses.""" +ExecutionOrScheduleID = WorkflowExecutionID | WorkflowScheduleID + class EventHistoryType(StrEnum): """The event types we care about.""" diff --git a/tracecat/workflow/executions/router.py b/tracecat/workflow/executions/router.py index 8db77b748..cd190b5fb 100644 --- a/tracecat/workflow/executions/router.py +++ b/tracecat/workflow/executions/router.py @@ -12,14 +12,15 @@ from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession -from tracecat import identifiers from tracecat.auth.credentials import authenticate_user_for_workspace from tracecat.db.engine import get_async_session from tracecat.db.schemas import WorkflowDefinition from tracecat.dsl.common import DSLInput +from tracecat.identifiers import WorkflowID from tracecat.logger import logger from tracecat.types.auth import Role from tracecat.types.exceptions import TracecatValidationError +from tracecat.workflow.executions.dependencies import UnquotedExecutionOrScheduleID from tracecat.workflow.executions.models import ( CreateWorkflowExecutionParams, CreateWorkflowExecutionResponse, @@ -36,7 +37,7 @@ async def list_workflow_executions( role: Annotated[Role, Depends(authenticate_user_for_workspace)], # Filters - workflow_id: identifiers.WorkflowID | None = Query(None), + workflow_id: WorkflowID | None = Query(None), ) -> list[WorkflowExecutionResponse]: """List all workflow executions.""" with logger.contextualize(role=role): @@ -54,7 +55,7 @@ async def list_workflow_executions( @router.get("/{execution_id}", tags=["workflow-executions"]) async def get_workflow_execution( role: Annotated[Role, Depends(authenticate_user_for_workspace)], - execution_id: identifiers.WorkflowExecutionID | identifiers.WorkflowScheduleID, + execution_id: UnquotedExecutionOrScheduleID, ) -> WorkflowExecutionResponse: """Get a workflow execution.""" with logger.contextualize(role=role): @@ -66,7 +67,7 @@ async def get_workflow_execution( @router.get("/{execution_id}/history", tags=["workflow-executions"]) async def list_workflow_execution_event_history( role: Annotated[Role, Depends(authenticate_user_for_workspace)], - execution_id: identifiers.WorkflowExecutionID | identifiers.WorkflowScheduleID, + execution_id: UnquotedExecutionOrScheduleID, ) -> list[EventHistoryResponse]: """Get a workflow execution.""" with logger.contextualize(role=role): @@ -126,7 +127,7 @@ async def create_workflow_execution( ) async def cancel_workflow_execution( role: Annotated[Role, Depends(authenticate_user_for_workspace)], - execution_id: identifiers.WorkflowExecutionID | identifiers.WorkflowScheduleID, + execution_id: UnquotedExecutionOrScheduleID, ) -> None: """Get a workflow execution.""" with logger.contextualize(role=role): @@ -150,7 +151,7 @@ async def cancel_workflow_execution( ) async def terminate_workflow_execution( role: Annotated[Role, Depends(authenticate_user_for_workspace)], - execution_id: identifiers.WorkflowExecutionID | identifiers.WorkflowScheduleID, + execution_id: UnquotedExecutionOrScheduleID, params: TerminateWorkflowExecutionParams, ) -> None: """Get a workflow execution."""