Skip to content
Draft
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
62 changes: 62 additions & 0 deletions frontend/src/client/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14974,6 +14974,40 @@ export const $WorkflowDefinitionReadMinimal = {
title: "WorkflowDefinitionReadMinimal",
} as const

export const $WorkflowRelationRead = {
properties: {
id: {
type: "string",
title: "Id",
},
title: {
anyOf: [
{
type: "string",
},
{
type: "null",
},
],
title: "Title",
},
alias: {
anyOf: [
{
type: "string",
},
{
type: "null",
},
],
title: "Alias",
},
},
type: "object",
required: ["id"],
title: "WorkflowRelationRead",
} as const

export const $WorkflowDirectoryItem = {
properties: {
id: {
Expand Down Expand Up @@ -15071,6 +15105,20 @@ export const $WorkflowDirectoryItem = {
},
],
},
parents: {
anyOf: [
{
items: {
$ref: "#/components/schemas/WorkflowRelationRead",
},
type: "array",
},
{
type: "null",
},
],
title: "Parents",
},
folder_id: {
anyOf: [
{
Expand All @@ -15083,6 +15131,20 @@ export const $WorkflowDirectoryItem = {
],
title: "Folder Id",
},
subflows: {
anyOf: [
{
items: {
$ref: "#/components/schemas/WorkflowRelationRead",
},
type: "array",
},
{
type: "null",
},
],
title: "Subflows",
},
type: {
type: "string",
const: "workflow",
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4790,6 +4790,12 @@ export type WorkflowDefinitionReadMinimal = {
created_at: string
}

export type WorkflowRelationRead = {
id: string
title?: string | null
alias?: string | null
}

export type WorkflowDirectoryItem = {
id: string
title: string
Expand All @@ -4804,6 +4810,8 @@ export type WorkflowDirectoryItem = {
error_handler?: string | null
latest_definition?: WorkflowDefinitionReadMinimal | null
folder_id?: string | null
parents?: Array<WorkflowRelationRead> | null
subflows?: Array<WorkflowRelationRead> | null
type: "workflow"
}

Expand Down
68 changes: 68 additions & 0 deletions frontend/src/components/dashboard/workflow-folders-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import type {
ApiError,
FolderDirectoryItem,
TagRead,
WorkflowRelationRead,
WorkflowDirectoryItem,
WorkflowReadMinimal,
} from "@/client"
import Link from "next/link"
import { DeleteWorkflowAlertDialog } from "@/components/dashboard/delete-workflow-dialog"
import { FolderDeleteAlertDialog } from "@/components/dashboard/folder-delete-dialog"
import { FolderMoveDialog } from "@/components/dashboard/folder-move-dialog"
Expand Down Expand Up @@ -423,6 +426,71 @@ export function WorkflowsDashboardTable({
},
enableHiding: true,
},
{
id: "Relations",
accessorFn: (row: DirectoryItem) =>
row.type === "workflow" ? (row as WorkflowDirectoryItem) : undefined,
header: ({ column }) => (
<DataTableColumnHeader
className="text-xs"
column={column}
title="Relations"
/>
),
cell: ({ row }) => {
if (row.original.type === "folder") {
return (
<span className="text-xs text-muted-foreground/70">
{NO_DATA}
</span>
)
}

const { parents, subflows } = row.original as WorkflowDirectoryItem

if (!(parents?.length || subflows?.length)) {
return (
<span className="text-xs text-muted-foreground/70">
No relations
</span>
)
}

const renderRelationBadge = (
relation: WorkflowRelationRead,
label: string
) => (
<Badge
key={`${label}-${relation.id}`}
asChild
variant="secondary"
className="gap-1 px-1.5 py-1 text-[0.65rem]"
>
<Link
href={`/workspaces/${workspaceId}/workflows/${relation.id}`}
>
<span className="font-semibold uppercase text-muted-foreground">
{label}
</span>
<span className="font-mono font-medium text-foreground/80">
{relation.alias || relation.title || relation.id}
</span>
</Link>
</Badge>
)

return (
<div className="flex flex-wrap gap-1">
{parents?.map((parent) =>
renderRelationBadge(parent, "Parent")
)}
{subflows?.map((subflow) =>
renderRelationBadge(subflow, "Subflow")
)}
</div>
)
},
},
{
id: "actions",
cell: ({ row }) => {
Expand Down
8 changes: 8 additions & 0 deletions tracecat/workflow/management/folders/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
from tracecat.workflow.management.schemas import WorkflowReadMinimal


class WorkflowRelationRead(BaseModel):
id: str
title: str | None = None
alias: str | None = None


class WorkflowFolderRead(BaseModel):
id: uuid.UUID
name: str
Expand Down Expand Up @@ -40,6 +46,8 @@ class FolderDirectoryItem(WorkflowFolderRead):

class WorkflowDirectoryItem(WorkflowReadMinimal):
type: Literal["workflow"]
parents: list[WorkflowRelationRead] | None = None
subflows: list[WorkflowRelationRead] | None = None


DirectoryItem = Annotated[
Expand Down
72 changes: 72 additions & 0 deletions tracecat/workflow/management/folders/service.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
from __future__ import annotations

import uuid
from collections.abc import Sequence
from typing import Literal

import yaml

import sqlalchemy as sa
from sqlalchemy import and_, cast, func, or_, select
from sqlalchemy.orm import selectinload

from tracecat.auth.types import Role
from tracecat.db.models import Workflow, WorkflowDefinition, WorkflowFolder
from tracecat.exceptions import (
TracecatAuthorizationError,
TracecatNotFoundError,
TracecatValidationError,
)
from tracecat.identifiers import WorkflowID
from tracecat.identifiers.workflow import WorkflowUUID
from tracecat.service import BaseService
from tracecat.tags.schemas import TagRead
from tracecat.workflow.management.folders.schemas import (
DirectoryItem,
FolderDirectoryItem,
WorkflowDirectoryItem,
WorkflowRelationRead,
)
from tracecat.workflow.management.schemas import WorkflowDefinitionReadMinimal

Check failure on line 30 in tracecat/workflow/management/folders/service.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

tracecat/workflow/management/folders/service.py:1:1: I001 Import block is un-sorted or un-formatted


class WorkflowFolderService(BaseService):
Expand Down Expand Up @@ -523,6 +526,7 @@

workflow_result = await self.session.execute(workflow_statement)
workflows_with_defns = workflow_result.tuples().all()
relations = await self._get_workflow_relations()
# For root path, get workflows with no folder_id
if path == "/":
# Get root-level folders
Expand Down Expand Up @@ -577,6 +581,9 @@
)
else:
latest_definition = None
workflow_relations = relations.get(
workflow.id, {"parents": [], "subflows": []}
)
directory_items.append(
WorkflowDirectoryItem(
type="workflow",
Expand All @@ -594,7 +601,72 @@
TagRead.model_validate(tag, from_attributes=True)
for tag in workflow.tags
],
parents=workflow_relations["parents"],
subflows=workflow_relations["subflows"],
)
)

return directory_items

async def _get_workflow_relations(
self,
) -> dict[uuid.UUID, dict[str, list[WorkflowRelationRead]]]:
"""Return parent/subflow relations for workflows in the workspace."""

workflows_stmt = select(Workflow).where(
Workflow.owner_id == self.workspace_id
).options(selectinload(Workflow.actions))
workflows_result = await self.session.execute(workflows_stmt)
workflows = workflows_result.scalars().unique().all()

alias_index: dict[str, Workflow] = {
workflow.alias: workflow for workflow in workflows if workflow.alias
}
id_index: dict[uuid.UUID, Workflow] = {workflow.id: workflow for workflow in workflows}

relation_map: dict[uuid.UUID, dict[str, list[WorkflowRelationRead]]] = {
workflow.id: {"parents": [], "subflows": []} for workflow in workflows
}

for workflow in workflows:
parent_short_id = WorkflowUUID.new(workflow.id).short()
for action in workflow.actions:
if action.type != "core.workflow.execute":
continue

try:
action_inputs = yaml.safe_load(action.inputs) or {}
except yaml.YAMLError:
continue

target_alias = action_inputs.get("workflow_alias")
target_id_raw = action_inputs.get("workflow_id")

target_workflow: Workflow | None = None
if target_alias and target_alias in alias_index:
target_workflow = alias_index[target_alias]
elif target_id_raw:
try:
target_uuid = WorkflowUUID.new(target_id_raw)
target_workflow = id_index.get(target_uuid.uuid())
except Exception:
continue

if not target_workflow:
continue

target_relation = WorkflowRelationRead(
id=WorkflowUUID.new(target_workflow.id).short(),
alias=target_workflow.alias,
title=target_workflow.title,
)
relation_map[workflow.id]["subflows"].append(target_relation)
relation_map[target_workflow.id]["parents"].append(
WorkflowRelationRead(
id=parent_short_id,
alias=workflow.alias,
title=workflow.title,
)
)

return relation_map
Loading